feat(接口测试): 接口调试-执行+保存接口

This commit is contained in:
baiqi 2024-02-07 18:00:19 +08:00 committed by Craftsman
parent 31c9897746
commit ac9a0e5c78
44 changed files with 1450 additions and 787 deletions

View File

@ -1,29 +0,0 @@
import Footer from '@/components/pure/footer/index.vue';
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
describe('Footer', () => {
test('renders the correct text', () => {
const wrapper = mount(Footer, {
props: {
text: 'Custom Text',
},
});
expect(wrapper.text()).toBe('Custom Text');
});
test('renders the default text if no prop is provided', () => {
const wrapper = mount(Footer);
expect(wrapper.text()).toBe('MeterSphere');
});
test('applies the correct styles', () => {
const wrapper = mount(Footer);
expect(wrapper.find('.footer').exists()).toBe(true);
expect(wrapper.classes()).toContain('footer');
});
});

View File

@ -1,99 +0,0 @@
import { nextTick } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getTableList } from '@/api/modules/api-test/index';
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
},
{
title: '接口名称',
dataIndex: 'name',
},
{
title: '请求类型',
dataIndex: 'method',
},
{
title: '责任人',
dataIndex: 'username',
},
{
title: '路径',
dataIndex: 'path',
},
{
title: '标签',
dataIndex: 'tags',
},
{
title: '更新时间',
slotName: 'updataTime',
},
{
title: '用例数',
dataIndex: 'caseTotal',
},
{
title: '用例状态',
dataIndex: 'caseStatus',
},
{
title: '用例通过率',
dataIndex: 'casePassingRate',
},
{
title: '接口状态',
dataIndex: 'status',
},
{
title: '创建时间',
slotName: 'createTime',
},
{
title: '描述',
dataIndex: 'description',
},
{
title: '操作',
slotName: 'action',
fixed: 'right',
width: 200,
},
];
describe('MS-Table', () => {
test('init table with useTable', async () => {
const { propsRes, propsEvent, loadList, setProps } = useTable(getTableList, {
columns,
scroll: { y: 750, x: 2000 },
selectable: true,
});
const wrapper = mount(MsBaseTable as any, {
vOn: propsEvent,
vBind: propsRes,
});
loadList();
await nextTick();
let content = wrapper.find('.arco-table-td-content').element.innerHTML;
expect(propsRes.value.data.length).toBe(20);
expect(content).toBe('e7bd7179-d63a-43a5-1a65-218473ee69ca');
setProps({});
loadList();
await nextTick();
content = wrapper.find('.arco-table-td-content').element.innerHTML;
expect(content).toBe('937be890-79bb-1b68-e03e-7d37a8b0a607');
});
});

View File

@ -1,121 +0,0 @@
import logo from '@/assets/svg/logo.svg';
import {
isArray,
isBlob,
isEmptyObject,
isExist,
isFile,
isFunction,
isNull,
isNumber,
isObject,
isRegExp,
isString,
isUndefined,
} from '@/utils/is';
import { describe, expect, test } from 'vitest';
describe('Is tool', () => {
test('isArray', () => {
const res = isArray([]);
expect(res).toBe(true);
const res2 = isArray('');
expect(res2).toBe(false);
});
test('isObject', () => {
const res = isObject({ a: 'a' });
expect(res).toBe(true);
const res2 = isObject([]);
expect(res2).toBe(false);
});
test('isEmptyObject', () => {
const res = isEmptyObject({});
expect(res).toBe(true);
const res2 = isEmptyObject({ a: 'a' });
expect(res2).toBe(false);
const res3 = isEmptyObject([]);
expect(res3).toBe(false);
});
test('isExist', () => {
const res = isExist(0);
expect(res).toBe(true);
const res2 = isExist(null);
expect(res2).toBe(false);
});
test('isFunction', () => {
const res = isFunction(() => ({}));
expect(res).toBe(true);
const res2 = isFunction({});
expect(res2).toBe(false);
});
test('isNull', () => {
const res = isNull(null);
expect(res).toBe(true);
const res2 = isNull(undefined);
expect(res2).toBe(false);
});
test('isUndefined', () => {
const res = isUndefined(undefined);
expect(res).toBe(true);
const res2 = isUndefined(null);
expect(res2).toBe(false);
});
test('isNumber', () => {
const res = isNumber(0);
expect(res).toBe(true);
const res2 = isNumber(null);
expect(res2).toBe(false);
});
test('isString', () => {
const res = isString('');
expect(res).toBe(true);
const res2 = isString(0);
expect(res2).toBe(false);
});
test('isRegExp', () => {
const res = isRegExp(/^a/);
expect(res).toBe(true);
const res2 = isRegExp('');
expect(res2).toBe(false);
});
test('isFile', () => {
const file = new File([logo], 'logo.svg');
const res = isFile(file);
expect(res).toBe(true);
const res2 = isFile({});
expect(res2).toBe(false);
});
test('isBlob', () => {
const blob = new Blob();
const res = isBlob(blob);
expect(res).toBe(true);
const res2 = isBlob(logo);
expect(res2).toBe(false);
});
});

View File

@ -47,6 +47,7 @@
"@tiptap/suggestion": "^2.1.13", "@tiptap/suggestion": "^2.1.13",
"@tiptap/vue-3": "^2.1.13", "@tiptap/vue-3": "^2.1.13",
"@types/color": "^3.0.4", "@types/color": "^3.0.4",
"@types/node": "^20.11.16",
"@vueuse/core": "^10.4.1", "@vueuse/core": "^10.4.1",
"@xmldom/xmldom": "^0.8.10", "@xmldom/xmldom": "^0.8.10",
"ace-builds": "^1.24.2", "ace-builds": "^1.24.2",

View File

@ -0,0 +1,66 @@
import MSR from '@/api/http/index';
import {
AddApiDebugUrl,
AddDebugModuleUrl,
DeleteDebugModuleUrl,
ExecuteApiDebugUrl,
GetDebugModuleCountUrl,
GetDebugModulesUrl,
MoveDebugModuleUrl,
UpdateApiDebugUrl,
UpdateDebugModuleUrl,
} from '@/api/requrls/api-test/debug';
import {
AddDebugModuleParams,
ExecuteRequestParams,
SaveDebugParams,
UpdateDebugModule,
UpdateDebugParams,
} from '@/models/apiTest/debug';
import { ModuleTreeNode, MoveModules } from '@/models/common';
// 获取模块树
export function getDebugModules() {
return MSR.get<ModuleTreeNode[]>({ url: GetDebugModulesUrl });
}
// 删除模块
export function deleteDebugModule(deleteId: string) {
return MSR.get({ url: DeleteDebugModuleUrl, params: deleteId });
}
// 添加模块
export function addDebugModule(data: AddDebugModuleParams) {
return MSR.post({ url: AddDebugModuleUrl, data });
}
// 移动模块
export function moveDebugModule(data: MoveModules) {
return MSR.post({ url: MoveDebugModuleUrl, data });
}
// 更新模块
export function updateDebugModule(data: UpdateDebugModule) {
return MSR.post({ url: UpdateDebugModuleUrl, data });
}
// 模块数量统计
export function getDebugModuleCount(data: { keyword: string }) {
return MSR.post({ url: GetDebugModuleCountUrl, data });
}
// 执行调试
export function executeDebug(data: ExecuteRequestParams) {
return MSR.post<ExecuteRequestParams>({ url: ExecuteApiDebugUrl, data });
}
// 新增调试
export function addDebug(data: SaveDebugParams) {
return MSR.post({ url: AddApiDebugUrl, data });
}
// 更新调试
export function updateDebug(data: UpdateDebugParams) {
return MSR.post({ url: UpdateApiDebugUrl, data });
}

View File

@ -1,16 +0,0 @@
import MSR from '@/api/http/index';
import { GetApiTestList, GetApiTestListUrl } from '@/api/requrls/api-test';
import { CommonList, TableQueryParams } from '@/models/common';
export function getTableList(params: TableQueryParams) {
const { current, pageSize, sort, filter, keyword } = params;
return MSR.post<CommonList<any>>({
url: GetApiTestList,
data: { current, pageSize, sort, filter, keyword, projectId: 'test-project-id' },
});
}
export function getlist() {
return MSR.get<CommonList<any>>({ url: GetApiTestListUrl });
}

View File

@ -0,0 +1,19 @@
import MSR from '@/api/http/index';
import { GetPluginOptionsUrl, GetPluginScriptUrl, GetProtocolListUrl } from '@/api/requrls/api-test/management';
import { GetPluginOptionsParams, PluginOption, ProtocolItem } from '@/models/apiTest/common';
// 获取协议列表
export function getProtocolList(organizationId: string) {
return MSR.get<ProtocolItem[]>({ url: GetProtocolListUrl, params: organizationId });
}
// 获取插件表单选项
export function getPluginOptions(data: GetPluginOptionsParams) {
return MSR.get<PluginOption[]>({ url: GetPluginOptionsUrl, data });
}
// 获取插件配置
export function getPluginScript(pluginId: string) {
return MSR.get({ url: GetPluginScriptUrl, params: pluginId });
}

View File

@ -0,0 +1,9 @@
export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试
export const AddApiDebugUrl = '/api/debug/add'; // 新增调试
export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试
export const UpdateDebugModuleUrl = '/api/debug/module/update'; // 更新模块
export const MoveDebugModuleUrl = '/api/debug/module/move'; // 移动模块
export const GetDebugModuleCountUrl = '/api/debug/module/count'; // 模块统计数量
export const AddDebugModuleUrl = '/api/debug/module/add'; // 添加模块
export const GetDebugModulesUrl = '/api/debug/module/tree'; // 查询模块树
export const DeleteDebugModuleUrl = '/api/debug/module/delete'; // 删除模块

View File

@ -0,0 +1,3 @@
export const GetProtocolListUrl = '/api/test/protocol'; // 获取协议列表
export const GetPluginOptionsUrl = '/api/test/plugin/form/option'; // 获取插件表单选项
export const GetPluginScriptUrl = '/api/test/plugin/script'; // 获取插件表单选项

View File

@ -369,6 +369,7 @@
} }
} }
.arco-checkbox { .arco-checkbox {
padding-left: 0;
.arco-checkbox-icon { .arco-checkbox-icon {
border: 1px solid var(--color-text-input-border); border: 1px solid var(--color-text-input-border);
} }

View File

@ -6,7 +6,7 @@
v-model:model-value="currentOrg" v-model:model-value="currentOrg"
:options="orgOptions" :options="orgOptions"
:loading="orgLoading" :loading="orgLoading"
class="w-[300px]" class="!w-[300px]"
@change="handleOrgChange" @change="handleOrgChange"
/> />
</div> </div>

View File

@ -49,6 +49,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineModel } from 'vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '../ms-tag/ms-tag.vue'; import MsTag from '../ms-tag/ms-tag.vue';
import FilterForm from './FilterForm.vue'; import FilterForm from './FilterForm.vue';

View File

@ -91,7 +91,7 @@
}); });
// //
editor.onDidBlurEditorText(() => { editor.onDidChangeModelContent(() => {
const value = editor.getValue(); // const value = editor.getValue(); //
emit('update:modelValue', value); emit('update:modelValue', value);
emit('change', value); emit('change', value);
@ -219,6 +219,13 @@
} }
} }
function format() {
if (editor) {
//
editor.getAction('editor.action.formatDocument')?.run();
}
}
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue) => { (newValue) => {
@ -274,6 +281,7 @@
insertContent, insertContent,
undo, undo,
redo, redo,
format,
}; };
}, },
}); });

View File

@ -22,6 +22,8 @@
v-if="attrs.selectable && props.selectedKeys" v-if="attrs.selectable && props.selectedKeys"
:width="props.firstColumnWidth || 60" :width="props.firstColumnWidth || 60"
fixed="left" fixed="left"
cell-class="arco-table-operation"
body-cell-class="arco-table-operation"
> >
<template #title> <template #title>
<SelectALL <SelectALL
@ -322,11 +324,11 @@
// - // -
const selectTotal = computed(() => { const selectTotal = computed(() => {
const { selectorStatus } = props; const { selectorStatus } = props;
if (!attrs.showPagination) {
// total
return (attrs.data as MsTableDataItem<TableData>[]).length;
}
if (selectorStatus === SelectAllEnum.CURRENT) { if (selectorStatus === SelectAllEnum.CURRENT) {
if (!attrs.showPagination) {
// total
return (attrs.data as MsTableDataItem<TableData>[]).length;
}
const { pageSize, total } = attrs.msPagination as MsPaginationI; const { pageSize, total } = attrs.msPagination as MsPaginationI;
if (pageSize > total) { if (pageSize > total) {
return total; return total;
@ -606,6 +608,11 @@
background-color: var(--color-text-n9); background-color: var(--color-text-n9);
} }
} }
:deep(.arco-table-operation) {
.arco-table-td-content {
@apply justify-center;
}
}
:deep(.ms-table-select-all) { :deep(.ms-table-select-all) {
.dropdown-icon { .dropdown-icon {
background: none !important; background: none !important;

View File

@ -465,5 +465,6 @@ export default function useTableProps<T>(
getSelectedCount, getSelectedCount,
resetSelector, resetSelector,
getTableQueryParams, getTableQueryParams,
setTableSelected,
}; };
} }

View File

@ -1,12 +1,23 @@
import { ConditionType } from '@/models/apiTest/debug'; import { ConditionType } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestConditionProcessor } from '@/enums/apiEnum';
// 条件操作类型 // 条件操作类型
export type ConditionTypeNameMap = Record<ConditionType, string>; export type ConditionTypeNameMap = Record<ConditionType, string>;
export const conditionTypeNameMap = { export const conditionTypeNameMap = {
script: 'apiTestDebug.script', [RequestConditionProcessor.SCRIPT]: 'apiTestDebug.script',
sql: 'apiTestDebug.sql', [RequestConditionProcessor.SQL]: 'apiTestDebug.sql',
waitTime: 'apiTestDebug.waitTime', [RequestConditionProcessor.TIME_WAITING]: 'apiTestDebug.waitTime',
extract: 'apiTestDebug.extractParameter', [RequestConditionProcessor.EXTRACT]: 'apiTestDebug.extractParameter',
}; };
// 代码字符集 // 代码字符集
export const codeCharset = ['UTF-8', 'UTF-16', 'GBK', 'GB2312', 'ISO-8859-1', 'Shift_JIS', 'ASCII', 'BIG5', 'KOI8-R']; export const codeCharset = ['UTF-8', 'UTF-16', 'GBK', 'GB2312', 'ISO-8859-1', 'Shift_JIS', 'ASCII', 'BIG5', 'KOI8-R'];
// 请求体类型显示映射
export const requestBodyTypeMap = {
[RequestBodyFormat.FORM_DATA]: 'form-data',
[RequestBodyFormat.WWW_FORM]: 'x-www-form-urlencoded',
[RequestBodyFormat.RAW]: 'raw',
[RequestBodyFormat.BINARY]: 'binary',
[RequestBodyFormat.JSON]: 'json',
[RequestBodyFormat.XML]: 'xml',
[RequestBodyFormat.NONE]: 'none',
};

View File

@ -11,6 +11,7 @@ export enum RequestMethods {
} }
// 接口组成部分 // 接口组成部分
export enum RequestComposition { export enum RequestComposition {
PLUGIN = 'PLUGIN',
HEADER = 'HEADER', HEADER = 'HEADER',
BODY = 'BODY', BODY = 'BODY',
QUERY = 'QUERY', QUERY = 'QUERY',
@ -23,13 +24,13 @@ export enum RequestComposition {
} }
// 接口请求体格式 // 接口请求体格式
export enum RequestBodyFormat { export enum RequestBodyFormat {
NONE = 'none', NONE = 'NONE',
FORM_DATA = 'form-data', FORM_DATA = 'FORM_DATA',
X_WWW_FORM_URLENCODED = 'x-www-form-urlencoded', WWW_FORM = 'WWW_FORM',
JSON = 'json', JSON = 'JSON',
XML = 'xml', XML = 'XML',
RAW = 'raw', RAW = 'RAW',
BINARY = 'binary', BINARY = 'BINARY',
} }
// 接口响应体格式 // 接口响应体格式
export enum RequestContentTypeEnum { export enum RequestContentTypeEnum {
@ -63,3 +64,124 @@ export enum RequestDefinitionStatus {
export enum RequestImportFormat { export enum RequestImportFormat {
SWAGGER = 'SWAGGER', SWAGGER = 'SWAGGER',
} }
// 接口认证设置类型
export enum RequestAuthType {
BASIC = 'BASIC',
DIGEST = 'DIGEST',
NONE = 'NONE',
}
// 接口参数表格的参数类型
export enum RequestParamsType {
STRING = 'string',
INTEGER = 'integer',
NUMBER = 'number',
ARRAY = 'array',
JSON = 'json',
FILE = 'file',
}
// 接口断言类型
export enum ResponseAssertionType {
RESPONSE_CODE = 'RESPONSE_CODE',
RESPONSE_HEADER = 'RESPONSE_HEADER',
RESPONSE_BODY = 'RESPONSE_BODY',
RESPONSE_TIME = 'RESPONSE_TIME',
SCRIPT = 'SCRIPT',
VARIABLE = 'VARIABLE',
}
// 接口断言-响应体断言类型
export enum ResponseBodyAssertionType {
DOCUMENT = 'DOCUMENT',
JSON_PATH = 'JSON_PATH',
REGEX = 'REGEX', // 正则表达式
XPATH = 'XPATH',
}
// 接口断言-响应体断言-文档类型
export enum ResponseBodyAssertionDocumentType {
JSON = 'JSON',
XML = 'XML',
}
// 接口断言-响应体断言-文档断言类型
export enum ResponseBodyDocumentAssertionType {
ARRAY = 'array',
BOOLEAN = 'boolean',
INTEGER = 'integer',
NUMBER = 'number',
STRING = 'string',
}
// 接口断言-响应体断言-Xpath断言类型
export enum ResponseBodyXPathAssertionFormat {
HTML = 'HTML',
XML = 'XML',
}
// 接口断言-断言匹配条件
export enum RequestAssertionCondition {
CONTAINS = 'CONTAINS', // 包含
EMPTY = 'EMPTY', // 为空
END_WITH = 'END_WITH', // 以 xx 结尾
EQUALS = 'EQUALS', // 等于
GT = 'GT', // 大于
GT_OR_EQUALS = 'GT_OR_EQUALS', // 大于等于
LENGTH_EQUALS = 'LENGTH_EQUALS', // 长度等于
LENGTH_GT = 'LENGTH_GT', // 长度大于
LENGTH_GT_OR_EQUALS = 'LENGTH_GT_OR_EQUALS', // 长度大于等于
LENGTH_LT = 'LENGTH_LT', // 长度小于
LENGTH_LT_OR_EQUALS = 'LENGTH_LT_OR_EQUALS', // 长度小于等于
LENGTH_NOT_EQUALS = 'LENGTH_NOT_EQUALS', // 长度不等于
LT = 'LT', // 小于
LT_OR_EQUALS = 'LT_OR_EQUALS', // 小于等于
NOT_CONTAINS = 'NOT_CONTAINS', // 不包含
NOT_EMPTY = 'NOT_EMPTY', // 不为空
NOT_EQUALS = 'NOT_EQUALS', // 不等于
REGEX = 'REGEX', // 正则表达式
START_WITH = 'START_WITH', // 以 xx 开头
UNCHECKED = 'UNCHECKED', // 不校验
}
// 接口请求-前后置条件-处理器类型
export enum RequestConditionProcessor {
SCRIPT = 'SCRIPT', // 脚本操作
SQL = 'SQL', // SQL操作
TIME_WAITING = 'TIME_WAITING', // 等待时间
EXTRACT = 'EXTRACT', // 参数提取
}
// 接口请求-前后置条件-脚本处理器语言
export enum RequestConditionScriptLanguage {
BEANSHELL = 'BEANSHELL', // Beanshell
BEANSHELL_JSR233 = 'BEANSHELL_JSR233', // Beanshell JSR233
GROOVY = 'GROOVY', // Groovy
JAVASCRIPT = 'JAVASCRIPT', // JavaScript
PYTHON = 'PYTHON', // Python
}
// 接口请求-参数提取-环境类型
export enum RequestExtractEnvType {
ENVIRONMENT = 'ENVIRONMENT', // 环境参数
GLOBAL = 'GLOBAL', // 全局参数
TEMPORARY = 'TEMPORARY', // 临时参数
}
// 接口请求-参数提取-表达式类型
export enum RequestExtractExpressionEnum {
REGEX = 'REGEX', // 正则表达式
JSON_PATH = 'JSON_PATH', // JSONPath
X_PATH = 'X_PATH', // Xpath
}
// 接口请求-参数提取-表达式匹配规则类型
export enum RequestExtractExpressionRuleType {
EXPRESSION = 'EXPRESSION', // 匹配表达式
GROUP = 'GROUP', // 匹配组
}
// 接口请求-参数提取-提取范围
export enum RequestExtractScope {
BODY = 'BODY',
BODY_AS_DOCUMENT = 'BODY_AS_DOCUMENT',
UNESCAPED_BODY = 'UNESCAPED_BODY',
REQUEST_HEADERS = 'REQUEST_HEADERS',
RESPONSE_CODE = 'RESPONSE_CODE',
RESPONSE_HEADERS = 'RESPONSE_HEADERS',
RESPONSE_MESSAGE = 'RESPONSE_MESSAGE',
URL = 'URL',
}
// 接口请求-参数提取-表达式匹配结果规则类型
export enum RequestExtractResultMatchingRule {
ALL = 'ALL', // 全部匹配
RANDOM = 'RANDOM', // 随机匹配
SPECIFIC = 'SPECIFIC', // 指定匹配
}

View File

@ -0,0 +1,18 @@
// 获取插件表单选项参数
export interface GetPluginOptionsParams {
orgId: string;
pluginId: string;
optionMethod: string;
queryParam: Record<string, any>;
}
// 插件表单选项子项
export interface PluginOption {
text: string;
value: string;
}
// 协议列表子项
export interface ProtocolItem {
protocol: string;
polymorphicName: string;
pluginId: string;
}

View File

@ -1,25 +1,324 @@
import {
RequestAssertionCondition,
RequestAuthType,
RequestBodyFormat,
RequestConditionProcessor,
RequestConditionScriptLanguage,
RequestContentTypeEnum,
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule,
RequestExtractScope,
RequestMethods,
RequestParamsType,
ResponseAssertionType,
ResponseBodyAssertionDocumentType,
ResponseBodyAssertionType,
ResponseBodyDocumentAssertionType,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
// 条件操作类型 // 条件操作类型
export type ConditionType = 'script' | 'sql' | 'waitTime' | 'extract'; export type ConditionType = keyof typeof RequestConditionProcessor;
// 表达式类型 // 断言-匹配条件规则
export type ExpressionType = 'regular' | 'JSONPath' | 'XPath'; export type RequestAssertionConditionType = keyof typeof RequestAssertionCondition;
// 表达式配置 // 前后置条件-脚本语言类型
export interface ExpressionConfig { export type RequestConditionScriptLanguageType = keyof typeof RequestConditionScriptLanguage;
expression: string;
expressionType?: ExpressionType;
regexpMatchRule?: 'expression' | 'group'; // 正则表达式匹配规则
resultMatchRule?: 'random' | 'specify' | 'all'; // 结果匹配规则
specifyMatchNum?: number; // 指定匹配下标
xmlMatchContentType?: 'xml' | 'html'; // 响应内容格式
}
// 响应时间信息 // 响应时间信息
export interface ResponseTiming { export interface ResponseTiming {
ready: number; dnsLookupTime: number;
socketInit: number; tcpHandshakeTime: number;
dnsQuery: number; sslHandshakeTime: number;
tcpHandshake: number; socketInitTime: number;
sslHandshake: number; latency: number;
waitingTTFB: number; downloadTime: number;
downloadContent: number; transferStartTime: number;
deal: number; responseTime: number;
total: number; }
// key-value参数信息
export interface KeyValueParam {
key: string;
value: string;
}
// 接口请求公共参数集合信息
export interface ExecuteRequestCommonParam {
encode: boolean; // 是否编码
maxLength: number;
minLength: number;
paramType: keyof typeof RequestParamsType; // 参数类型
required: boolean;
description: string;
enable: boolean; // 参数是否启用
}
// 接口请求form-data、x-www-form-urlencoded参数集合信息
export type ExecuteRequestFormBodyFormValue = ExecuteRequestCommonParam & {
files?: {
fileId: string;
fileName: string;
}[];
contentType?: keyof typeof RequestContentTypeEnum & string;
};
export interface ExecuteRequestFormBody {
formValues: ExecuteRequestFormBodyFormValue[];
}
// 接口请求binary-body参数集合信息
export interface ExecuteBinaryBody {
description: string;
file?: {
fileId: string;
fileName: string;
};
}
// 接口请求json-body参数集合信息
export interface ExecuteJsonBody {
enableJsonSchema?: boolean;
enableTransition?: boolean;
jsonSchema?: string;
jsonValue: string;
}
// 接口请求-带开启关闭的参数集合信息
export interface EnableKeyValueParam extends KeyValueParam {
description: string;
enable: boolean; // 参数是否启用
}
// 执行请求配置
export interface ExecuteOtherConfig {
autoRedirects: boolean; // 是否自动重定向 默认 false
certificateAlias: string; // 证书别名
connectTimeout: number; // 连接超时时间
followRedirects: boolean; // 是否跟随重定向 默认 false
responseTimeout: number; // 响应超时时间
}
// 断言-断言公共信息
export interface ResponseAssertionCommon {
name: string; // 断言名称
enable: boolean; // 是否启用断言
assertionType: keyof typeof ResponseAssertionType; // 断言类型
}
// 断言-断言列表泛型
export interface ResponseAssertionGenerics<T> {
assertions: T[];
responseFormat?: keyof typeof ResponseBodyXPathAssertionFormat;
}
// 断言-响应头断言子项
export interface ResponseHeaderAssertionItem {
condition: RequestAssertionConditionType;
enable: boolean;
expectedValue: string;
header: string; // 响应头
}
// 断言-状态码断言
export type ResponseCodeAssertion = Pick<ResponseHeaderAssertionItem, 'condition' | 'expectedValue'>;
// 断言-文档断言-JSON断言\XML断言
export interface ResponseDocumentAssertionElement {
id?: string;
arrayVerification: boolean; // 是否组内验证
children: ResponseDocumentAssertionElement[];
condition: RequestAssertionConditionType;
expectedResult: Record<string, any>; // 匹配值 即预期结果
include: boolean; // 是否必含
paramName: string; // 参数名
type: keyof typeof ResponseBodyDocumentAssertionType; // 断言类型
typeVerification: boolean; // 是否类型验证
}
// 断言-文档断言
export interface ResponseDocumentAssertion {
enable: boolean; // 是否启用
documentType: keyof typeof ResponseBodyAssertionDocumentType; // 文档类型
followApiId: string; // 跟随定义的apiId 传空为不跟随接口定义
jsonAssertion: ResponseDocumentAssertionElement;
xmlAssertion: ResponseDocumentAssertionElement;
}
// 断言-断言列表的断言子项
export interface ResponseAssertionItem {
condition: RequestAssertionConditionType;
expectedValue: string;
expression: string;
enable?: boolean;
}
// 断言-JSONPath断言子项
export type ResponseJSONPathAssertionItem = ResponseAssertionItem;
// 断言-正则断言子项
export type ResponseRegexAssertionItem = Pick<ResponseAssertionItem, 'expression'>;
// 断言-Xpath断言子项
export type ResponseXPathAssertionItem = Pick<ResponseAssertionItem, 'expression' | 'expectedValue'>;
// 脚本公共配置
export interface ScriptCommonConfig {
enableCommonScript: boolean; // 是否启用公共脚本
script: string; // 脚本内容
scriptId: string; // 脚本id
scriptLanguage: RequestConditionScriptLanguageType; // 脚本语言
params: KeyValueParam[]; // 公共脚本参数
}
// 断言-响应体断言
export interface ResponseBodyAssertion {
assertionBodyType: keyof typeof ResponseBodyAssertionType; // 断言类型
documentAssertion: ResponseDocumentAssertion; // 文档断言
jsonPathAssertion: ResponseAssertionGenerics<ResponseJSONPathAssertionItem>; // JSONPath断言
regexAssertion: ResponseAssertionGenerics<ResponseRegexAssertionItem>; // 正则断言
xpathAssertion: ResponseAssertionGenerics<ResponseXPathAssertionItem>; // XPath断言
}
// 断言-响应时间断言
export type ResponseTimeAssertion = Pick<ResponseAssertionItem, 'expectedValue'>;
// 断言-脚本断言
export type ResponseScriptAssertion = ScriptCommonConfig;
// 断言-变量断言
export interface ResponseVariableAssertion {
variableAssertionItems: ResponseAssertionItem[];
}
// 执行请求-前后置条件处理器
export interface ExecuteConditionProcessorCommon {
enable: boolean; // 是否启用
name: string; // 请求名称
processorType: keyof typeof RequestConditionProcessor;
}
// 执行请求-前后置条件-脚本处理器
export type ScriptProcessor = ScriptCommonConfig;
// 执行请求-前后置条件-SQL脚本处理器
export interface SQLProcessor {
dataSourceId: string; // 数据源ID
environmentId: string; // 环境ID
queryTimeout: number; // 超时时间
resultVariable: string; // 按结果存储时的结果变量
script: string; // 脚本内容
variableNames: string; // 按列存储时的变量名集合,多个列可以使用逗号分隔
variables: EnableKeyValueParam[]; // 变量列表
extractParams: KeyValueParam[]; // 提取参数列表
}
// 执行请求-前后置条件-等待时间处理器
export interface TimeWaitingProcessor {
delay: number; // 等待时间 单位:毫秒
}
// 表达式类型
export type ExpressionType = keyof typeof RequestExtractExpressionEnum;
// 表达式配置
export interface ExpressionCommonConfig {
enable: boolean; // 是否启用
expression: string;
extractType: ExpressionType; // 表达式类型
variableName: string;
variableType: keyof typeof RequestExtractEnvType;
resultMatchingRule: keyof typeof RequestExtractResultMatchingRule; // 结果匹配规则
resultMatchingRuleNum: number; // 匹配第几条结果
}
// 正则提取配置
export interface RegexExtract extends ExpressionCommonConfig {
expressionMatchingRule: keyof typeof RequestExtractExpressionRuleType; // 正则表达式匹配规则
extractScope: keyof typeof RequestExtractScope; // 正则提取范围
}
// JSONPath提取配置
export type JSONPathExtract = ExpressionCommonConfig;
// XPath提取配置
export interface XPathExtract extends ExpressionCommonConfig {
responseFormat: keyof typeof ResponseBodyXPathAssertionFormat; // 响应格式
}
// 执行请求-前后置条件-参数提取处理器
export interface ExtractProcessor {
extractors: (RegexExtract | JSONPathExtract | XPathExtract)[];
}
// 执行请求-前后置条件配置
export type ExecuteConditionProcessor = ExecuteConditionProcessorCommon &
(ScriptProcessor | SQLProcessor | TimeWaitingProcessor | ExtractProcessor);
export interface ExecuteConditionConfig {
enableGlobal: boolean; // 是否启用全局前置 默认为 true
processors: ExecuteConditionProcessor[];
}
// 执行请求-断言配置子项
export type ExecuteAssertionItem = ResponseAssertionCommon &
ResponseCodeAssertion &
ResponseAssertionGenerics<ResponseHeaderAssertionItem> &
ResponseBodyAssertion &
ResponseTimeAssertion &
ResponseScriptAssertion &
ResponseVariableAssertion;
// 执行请求-断言配置
export interface ExecuteAssertionConfig {
enableGlobal: boolean; // 是否启用全局断言
assertions: ExecuteAssertionItem[];
}
// 执行请求-共用配置子项
export interface ExecuteCommonChild {
polymorphicName: 'MsCommonElement'; // 协议多态名称写死MsCommonElement
assertionConfig: ExecuteAssertionConfig;
postProcessorConfig: ExecuteConditionConfig; // 后置处理器配置
preProcessorConfig: ExecuteConditionConfig; // 前置处理器配置
}
// 执行请求-认证配置
export interface ExecuteAuthConfig {
authType: keyof typeof RequestAuthType;
password: string;
username: string;
}
// 执行请求- body 配置-文本格式的 body
export interface ExecuteValueBody {
value: string;
}
// 执行请求- body 配置
export interface ExecuteBody {
bodyType: keyof typeof RequestBodyFormat;
binaryBody: ExecuteBinaryBody;
formDataBody: ExecuteRequestFormBody;
jsonBody: ExecuteJsonBody;
rawBody: ExecuteValueBody;
wwwFormBody: ExecuteRequestFormBody;
xmlBody: ExecuteValueBody;
}
// 执行HTTP请求入参
export interface ExecuteHTTPRequestFullParams {
authConfig: ExecuteAuthConfig;
body: ExecuteBody;
headers: EnableKeyValueParam[];
method: keyof typeof RequestMethods;
otherConfig: ExecuteOtherConfig;
path: string;
query: ExecuteRequestCommonParam[];
rest: ExecuteRequestCommonParam[];
url: string;
polymorphicName: string; // 协议多态名称
children: ExecuteCommonChild[]; // 协议共有的子项配置
}
// 执行插件请求入参
export interface ExecutePluginRequestParams {
[key: string]: any; // key-value形式的插件参数
polymorphicName: string; // 协议多态名称
children: ExecuteCommonChild[]; // 协议共有的子项配置
}
// 执行接口调试入参
export interface ExecuteRequestParams {
id?: string;
reportId: string;
environmentId: string;
tempFileIds: string[];
request: ExecuteHTTPRequestFullParams | ExecutePluginRequestParams;
projectId: string;
}
// 保存接口调试入参
export interface SaveDebugParams {
name: string;
protocol: string;
method: keyof typeof RequestMethods;
path: string;
projectId: string;
moduleId: string;
request: ExecuteHTTPRequestFullParams | ExecutePluginRequestParams;
uploadFileIds: string[];
linkFileIds: string[];
}
// 更新接口调试入参
export interface UpdateDebugParams extends SaveDebugParams {
id: string;
deleteFileIds: string[];
unLinkRefIds: string[];
}
// 更新模块入参
export interface UpdateDebugModule {
id: string;
name: string;
}
// 添加模块入参
export interface AddDebugModuleParams {
projectId: string;
name: string;
parentId: string;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="condition-content"> <div class="condition-content">
<!-- 脚本操作 --> <!-- 脚本操作 -->
<template v-if="condition.type === 'script'"> <template v-if="condition.type === RequestConditionProcessor.SCRIPT">
<a-radio-group v-model:model-value="condition.scriptType" size="small" class="mb-[16px]"> <a-radio-group v-model:model-value="condition.scriptType" size="small" class="mb-[16px]">
<a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio> <a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio> <a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio>
@ -128,7 +128,7 @@
</div> </div>
</template> </template>
<!-- SQL操作 --> <!-- SQL操作 -->
<template v-else-if="condition.type === 'sql'"> <template v-else-if="condition.type === RequestConditionProcessor.SQL">
<div class="mb-[16px]"> <div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div> <div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input <a-input
@ -205,7 +205,7 @@
</div> </div>
</template> </template>
<!-- 等待时间 --> <!-- 等待时间 -->
<div v-else-if="condition.type === 'waitTime'"> <div v-else-if="condition.type === RequestConditionProcessor.TIME_WAITING">
<div class="mb-[8px] flex items-center"> <div class="mb-[8px] flex items-center">
{{ t('apiTestDebug.waitTime') }} {{ t('apiTestDebug.waitTime') }}
<div class="text-[var(--color-text-4)]">(ms)</div> <div class="text-[var(--color-text-4)]">(ms)</div>
@ -213,7 +213,7 @@
<a-input-number v-model:model-value="condition.time" mode="button" :step="100" :min="0" class="w-[160px]" /> <a-input-number v-model:model-value="condition.time" mode="button" :step="100" :min="0" class="w-[160px]" />
</div> </div>
<!-- 提取参数 --> <!-- 提取参数 -->
<div v-else-if="condition.type === 'extract'"> <div v-else-if="condition.type === RequestConditionProcessor.EXTRACT">
<paramTable <paramTable
ref="extractParamsTableRef" ref="extractParamsTableRef"
v-model:params="condition.extractParams" v-model:params="condition.extractParams"
@ -224,7 +224,7 @@
:response="props.response" :response="props.response"
:height-used="(props.heightUsed || 0) + 62" :height-used="(props.heightUsed || 0) + 62"
@change="handleExtractParamTableChange" @change="handleExtractParamTableChange"
@more-action-select="handleExtractParamMoreActionSelect" @more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
> >
<template #expression="{ record }"> <template #expression="{ record }">
<a-popover <a-popover
@ -320,7 +320,17 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug'; import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import {
RequestConditionProcessor,
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractResultMatchingRule,
RequestExtractScope,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{ const props = defineProps<{
data: Record<string, any>; data: Record<string, any>;
@ -490,8 +500,8 @@ org.apache.http.client.method . . . '' at line number 2
}, },
{ {
title: 'apiTestDebug.mode', title: 'apiTestDebug.mode',
dataIndex: 'expressionType', dataIndex: 'extractType',
slotName: 'expressionType', slotName: 'extractType',
typeOptions: [ typeOptions: [
{ {
label: t('apiTestDebug.regular'), label: t('apiTestDebug.regular'),
@ -510,8 +520,8 @@ org.apache.http.client.method . . . '' at line number 2
}, },
{ {
title: 'apiTestDebug.range', title: 'apiTestDebug.range',
dataIndex: 'range', dataIndex: 'extractScope',
slotName: 'range', slotName: 'extractScope',
typeOptions: [ typeOptions: [
{ {
label: 'Body', label: 'Body',
@ -581,22 +591,23 @@ org.apache.http.client.method . . . '' at line number 2
} }
const extractParamsTableRef = ref<InstanceType<typeof paramTable>>(); const extractParamsTableRef = ref<InstanceType<typeof paramTable>>();
const defaultExtractParamItem: Record<string, any> = { const defaultExtractParamItem: ExpressionConfig = {
name: '', enable: true,
type: 'temp', variableName: '',
range: 'body', variableType: RequestExtractEnvType.TEMPORARY,
extractScope: RequestExtractScope.BODY,
expression: '', expression: '',
expressionType: 'regular', extractType: RequestExtractExpressionEnum.REGEX,
regexpMatchRule: 'expression', regexpMatchRule: 'expression',
resultMatchRule: 'random', resultMatchingRule: RequestExtractResultMatchingRule.RANDOM,
specifyMatchNum: 1, resultMatchingRuleNum: 1,
xmlMatchContentType: 'xml', responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false, moreSettingPopoverVisible: false,
}; };
const fastExtractionVisible = ref(false); const fastExtractionVisible = ref(false);
const activeRecord = ref<any>({ ...defaultExtractParamItem }); // const activeRecord = ref({ ...defaultExtractParamItem }); //
function showFastExtraction(record: Record<string, any>) { function showFastExtraction(record: ExpressionConfig) {
activeRecord.value = { ...record }; activeRecord.value = { ...record };
fastExtractionVisible.value = true; fastExtractionVisible.value = true;
} }
@ -608,7 +619,7 @@ org.apache.http.client.method . . . '' at line number 2
/** /**
* 处理提取参数表格更多操作 * 处理提取参数表格更多操作
*/ */
function handleExtractParamMoreActionSelect(event: ActionsItem, record: Record<string, any>) { function handleExtractParamMoreActionSelect(event: ActionsItem, record: ExpressionConfig) {
activeRecord.value = { ...record }; activeRecord.value = { ...record };
if (event.eventTag === 'copy') { if (event.eventTag === 'copy') {
emit('copy'); emit('copy');
@ -620,7 +631,7 @@ org.apache.http.client.method . . . '' at line number 2
/** /**
* 提取参数表格-应用更多设置 * 提取参数表格-应用更多设置
*/ */
function applyMoreSetting(record: Record<string, any>) { function applyMoreSetting(record: ExpressionConfig) {
condition.value.extractParams = condition.value.extractParams.map((e) => { condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) { if (e.id === activeRecord.value.id) {
record.moreSettingPopoverVisible = false; record.moreSettingPopoverVisible = false;
@ -637,7 +648,7 @@ org.apache.http.client.method . . . '' at line number 2
/** /**
* 提取参数表格-保存快速提取的配置 * 提取参数表格-保存快速提取的配置
*/ */
function handleFastExtractionApply(config: ExpressionConfig) { function handleFastExtractionApply(config: RegexExtract | JSONPathExtract | XPathExtract) {
condition.value.extractParams = condition.value.extractParams.map((e) => { condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) { if (e.id === activeRecord.value.id) {
return { return {

View File

@ -17,7 +17,7 @@
<slot name="titleRight"></slot> <slot name="titleRight"></slot>
</div> </div>
</div> </div>
<div v-show="data.length > 0" class="flex h-[calc(100%-110px)] gap-[8px]"> <div v-show="data.length > 0" class="flex h-[calc(100%-40px)] gap-[8px]">
<div class="h-full w-[20%] min-w-[220px]"> <div class="h-full w-[20%] min-w-[220px]">
<conditionList <conditionList
v-model:list="data" v-model:list="data"
@ -47,6 +47,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ConditionType } from '@/models/apiTest/debug'; import { ConditionType } from '@/models/apiTest/debug';
import { RequestConditionProcessor } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
list: Array<Record<string, any>>; list: Array<Record<string, any>>;
@ -113,10 +114,10 @@ org.apache.http.client.method . . . '' at line number 2
function addPrecondition(value: string | number | Record<string, any> | undefined) { function addPrecondition(value: string | number | Record<string, any> | undefined) {
const id = new Date().getTime(); const id = new Date().getTime();
switch (value) { switch (value) {
case 'script': case RequestConditionProcessor.SCRIPT:
data.value.push({ data.value.push({
id, id,
type: 'script', type: RequestConditionProcessor.SCRIPT,
name: t('apiTestDebug.preconditionScriptName'), name: t('apiTestDebug.preconditionScriptName'),
scriptType: 'manual', scriptType: 'manual',
enable: true, enable: true,
@ -127,10 +128,10 @@ org.apache.http.client.method . . . '' at line number 2
}, },
}); });
break; break;
case 'sql': case RequestConditionProcessor.SQL:
data.value.push({ data.value.push({
id, id,
type: 'sql', type: RequestConditionProcessor.SQL,
desc: '', desc: '',
enable: true, enable: true,
sqlSource: { sqlSource: {
@ -141,18 +142,18 @@ org.apache.http.client.method . . . '' at line number 2
}, },
}); });
break; break;
case 'waitTime': case RequestConditionProcessor.TIME_WAITING:
data.value.push({ data.value.push({
id, id,
type: 'waitTime', type: RequestConditionProcessor.TIME_WAITING,
enable: true, enable: true,
time: 1000, time: 1000,
}); });
break; break;
case 'extract': case RequestConditionProcessor.EXTRACT:
data.value.push({ data.value.push({
id, id,
type: 'extract', type: RequestConditionProcessor.EXTRACT,
enable: true, enable: true,
extractParams: [], extractParams: [],
}); });

View File

@ -151,16 +151,16 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { matchXMLWithXPath } from '@/utils/xpath'; import { matchXMLWithXPath } from '@/utils/xpath';
import { ExpressionConfig } from '@/models/apiTest/debug'; import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
config: ExpressionConfig; config: (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
response?: string; // response?: string; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', value: boolean): void; (e: 'update:visible', value: boolean): void;
(e: 'apply', config: ExpressionConfig): void; (e: 'apply', config: (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -96,7 +96,9 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug'; import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{ const props = defineProps<{
config: ExpressionConfig; config: ExpressionConfig;

View File

@ -58,7 +58,7 @@
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]', record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]', '!mr-[4px] !p-[4px]',
]" ]"
@click="record.required = !record.required" @click="toggleRequired(record)"
> >
<div>*</div> <div>*</div>
</MsButton> </MsButton>
@ -186,11 +186,7 @@
:list="getMoreActionList(columnConfig.moreAction, record)" :list="getMoreActionList(columnConfig.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record)" @select="(e) => handleMoreActionSelect(e, record)"
/> />
<a-trigger <a-trigger v-if="columnConfig.format === RequestBodyFormat.FORM_DATA" trigger="click" position="br">
v-if="columnConfig.format && columnConfig.format !== RequestBodyFormat.X_WWW_FORM_URLENCODED"
trigger="click"
position="br"
>
<MsButton type="icon" class="mr-[8px]"><icon-more /></MsButton> <MsButton type="icon" class="mr-[8px]"><icon-more /></MsButton>
<template #content> <template #content>
<div class="content-type-trigger-content"> <div class="content-type-trigger-content">
@ -296,6 +292,7 @@
</template> </template>
<script async setup lang="ts"> <script async setup lang="ts">
import { useVModel } from '@vueuse/core';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
@ -313,7 +310,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
import { RequestBodyFormat, RequestContentTypeEnum } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
interface Param { interface Param {
@ -340,11 +337,12 @@
typeTitleTooltip?: string; // type tooltip typeTitleTooltip?: string; // type tooltip
hasEnable?: boolean; // operation enable hasEnable?: boolean; // operation enable
moreAction?: ActionsItem[]; // operation moreAction?: ActionsItem[]; // operation
format?: RequestBodyFormat | 'query' | 'rest'; // operation format?: RequestBodyFormat; // operation
}; };
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
selectedKeys?: string[];
params: any[]; params: any[];
defaultParamItem?: Partial<Param>; // defaultParamItem?: Partial<Param>; //
columns: ParamTableColumn[]; columns: ParamTableColumn[];
@ -372,7 +370,7 @@
defaultParamItem: () => ({ defaultParamItem: () => ({
required: false, required: false,
name: '', name: '',
type: 'string', type: RequestParamsType.STRING,
value: '', value: '',
min: undefined, min: undefined,
max: undefined, max: undefined,
@ -386,6 +384,7 @@
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:selectedKeys', value: string[]): void;
(e: 'change', data: any[], isInit?: boolean): void; // (e: 'change', data: any[], isInit?: boolean): void; //
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void; (e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
(e: 'projectChange', projectId: string): void; (e: 'projectChange', projectId: string): void;
@ -393,6 +392,8 @@
const { t } = useI18n(); const { t } = useI18n();
const innerSelectedKeys = useVModel(props, 'selectedKeys', emit);
const tableStore = useTableStore(); const tableStore = useTableStore();
async function initColumns() { async function initColumns() {
if (props.showSetting && props.tableKey) { if (props.showSetting && props.tableKey) {
@ -402,6 +403,7 @@
initColumns(); initColumns();
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), { const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 24,
tableKey: props.showSetting ? props.tableKey : undefined, tableKey: props.showSetting ? props.tableKey : undefined,
scroll: props.scroll, scroll: props.scroll,
heightUsed: props.heightUsed, heightUsed: props.heightUsed,
@ -412,8 +414,16 @@
disabled: props.disabled, disabled: props.disabled,
showSelectorAll: props.showSelectorAll, showSelectorAll: props.showSelectorAll,
isSimpleSetting: props.isSimpleSetting, isSimpleSetting: props.isSimpleSetting,
showPagination: false,
}); });
watch(
() => propsRes.value.selectedKeys,
(val) => {
innerSelectedKeys.value = Array.from(val);
}
);
watch( watch(
() => props.heightUsed, () => props.heightUsed,
(val) => { (val) => {
@ -447,10 +457,12 @@
? isEqual(lastData, props.defaultParamItem) ? isEqual(lastData, props.defaultParamItem)
: isEqual(lastData[key], props.defaultParamItem[key]); : isEqual(lastData[key], props.defaultParamItem[key]);
if (isForce || (val !== '' && !isNotChange)) { if (isForce || (val !== '' && !isNotChange)) {
const id = new Date().getTime().toString();
propsRes.value.data.push({ propsRes.value.data.push({
id: new Date().getTime(), id,
...props.defaultParamItem, ...props.defaultParamItem,
} as any); } as any);
propsRes.value.selectedKeys.add(id);
emit('change', propsRes.value.data); emit('change', propsRes.value.data);
} }
} }
@ -467,12 +479,14 @@
addTableLine(); addTableLine();
} }
} else { } else {
const id = new Date().getTime().toString();
propsRes.value.data = [ propsRes.value.data = [
{ {
id: new Date().getTime(), // id props.defaultParamItem id id, // id props.defaultParamItem id
...props.defaultParamItem, ...props.defaultParamItem,
}, },
] as any[]; ] as any[];
propsRes.value.selectedKeys.add(id);
emit('change', propsRes.value.data, true); emit('change', propsRes.value.data, true);
} }
}, },
@ -481,6 +495,11 @@
} }
); );
function toggleRequired(record: Record<string, any>) {
record.required = !record.required;
emit('change', propsRes.value.data);
}
const showQuickInputParam = ref(false); const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({}); const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref(''); const quickInputParamValue = ref('');
@ -500,7 +519,6 @@
activeQuickInputRecord.value.value = quickInputParamValue.value; activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false; showQuickInputParam.value = false;
clearQuickInputParam(); clearQuickInputParam();
addTableLine(quickInputParamValue.value, 'value', true);
emit('change', propsRes.value.data); emit('change', propsRes.value.data);
} }
@ -526,7 +544,6 @@
activeQuickInputRecord.value.desc = quickInputDescValue.value; activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false; showQuickInputDesc.value = false;
clearQuickInputDesc(); clearQuickInputDesc();
addTableLine(quickInputDescValue.value, 'desc', true);
emit('change', propsRes.value.data); emit('change', propsRes.value.data);
} }
@ -605,6 +622,11 @@
padding: 12px 2px; padding: 12px 2px;
} }
} }
:deep(.arco-table-col-fixed-right) {
.arco-table-cell-align-left {
padding: 16px;
}
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) { :deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) { &:not(:hover) {
border-color: transparent !important; border-color: transparent !important;

View File

@ -51,7 +51,7 @@
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { addReviewModule, updateReviewModule } from '@/api/modules/case-management/caseReview'; import { addDebugModule, updateDebugModule } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -123,7 +123,7 @@
loading.value = true; loading.value = true;
if (props.mode === 'add') { if (props.mode === 'add') {
// //
await addReviewModule({ await addDebugModule({
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
parentId: props.parentId || '', parentId: props.parentId || '',
name: form.value.field, name: form.value.field,
@ -132,7 +132,7 @@
emit('addFinish', form.value.field); emit('addFinish', form.value.field);
} else if (props.mode === 'rename') { } else if (props.mode === 'rename') {
// //
await updateReviewModule({ await updateDebugModule({
id: props.nodeId || '', id: props.nodeId || '',
name: form.value.field, name: form.value.field,
}); });

View File

@ -2,12 +2,11 @@
<div class="flex w-full gap-[8px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"> <div class="flex w-full gap-[8px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="text-item-wrapper"> <div class="text-item-wrapper">
<div class="light-item">{{ t('apiTestDebug.responseStage') }}</div> <div class="light-item">{{ t('apiTestDebug.responseStage') }}</div>
<div class="light-item">{{ t('apiTestDebug.ready') }}</div>
<div class="normal-item">{{ t('apiTestDebug.socketInit') }}</div>
<div class="normal-item">{{ t('apiTestDebug.dnsQuery') }}</div> <div class="normal-item">{{ t('apiTestDebug.dnsQuery') }}</div>
<div class="normal-item">{{ t('apiTestDebug.tcpHandshake') }}</div> <div class="normal-item">{{ t('apiTestDebug.tcpHandshake') }}</div>
<div class="normal-item">{{ t('apiTestDebug.sslHandshake') }}</div> <div class="normal-item">{{ t('apiTestDebug.sslHandshake') }}</div>
<div class="normal-item">{{ t('apiTestDebug.waitingTTFB') }}</div> <div class="normal-item">{{ t('apiTestDebug.socketInit') }}</div>
<!-- <div class="normal-item">{{ t('apiTestDebug.waitingTTFB') }}</div> -->
<div class="normal-item">{{ t('apiTestDebug.downloadContent') }}</div> <div class="normal-item">{{ t('apiTestDebug.downloadContent') }}</div>
<div class="light-item">{{ t('apiTestDebug.deal') }}</div> <div class="light-item">{{ t('apiTestDebug.deal') }}</div>
<div class="total-item">{{ t('apiTestDebug.total') }}</div> <div class="total-item">{{ t('apiTestDebug.total') }}</div>
@ -29,15 +28,14 @@
<a-divider direction="vertical" margin="0" /> <a-divider direction="vertical" margin="0" />
<div class="text-item-wrapper--right"> <div class="text-item-wrapper--right">
<div class="light-item">{{ t('apiTestDebug.time') }}</div> <div class="light-item">{{ t('apiTestDebug.time') }}</div>
<div class="light-item">{{ props.responseTiming.ready }} ms</div> <div class="normal-item">{{ props.responseTiming.dnsLookupTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.socketInit }} ms</div> <div class="normal-item">{{ props.responseTiming.tcpHandshakeTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.dnsQuery }} ms</div> <div class="normal-item">{{ props.responseTiming.sslHandshakeTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.tcpHandshake }} ms</div> <div class="normal-item">{{ props.responseTiming.socketInitTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.sslHandshake }} ms</div> <!-- <div class="normal-item">{{ props.responseTiming.latency }} ms</div> -->
<div class="normal-item">{{ props.responseTiming.waitingTTFB }} ms</div> <div class="normal-item">{{ props.responseTiming.downloadTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.downloadContent }} ms</div> <div class="light-item">{{ props.responseTiming.transferStartTime }} ms</div>
<div class="light-item">{{ props.responseTiming.deal }} ms</div> <div class="total-item">{{ props.responseTiming.responseTime }} ms</div>
<div class="total-item">{{ props.responseTiming.total }} ms</div>
</div> </div>
</div> </div>
</template> </template>
@ -58,13 +56,16 @@
const keys = Object.keys(props.responseTiming).filter((key) => key !== 'total'); const keys = Object.keys(props.responseTiming).filter((key) => key !== 'total');
let preLinesTotalLeft = 0; let preLinesTotalLeft = 0;
keys.forEach((key, index) => { keys.forEach((key, index) => {
const itemWidth = (props.responseTiming[key] / props.responseTiming.total) * 100; if (key !== 'responseTime' && key !== 'latency') {
arr.push({ // 100%
key, const itemWidth = (props.responseTiming[key] / props.responseTiming.responseTime) * 100;
width: `${itemWidth}%`, arr.push({
left: index !== 0 ? `${preLinesTotalLeft}%` : '', key,
}); width: `${itemWidth}%`,
preLinesTotalLeft += itemWidth; left: index !== 0 ? `${preLinesTotalLeft}%` : '',
});
preLinesTotalLeft += itemWidth;
}
}); });
return arr; return arr;
}); });

View File

@ -3,14 +3,14 @@
<div class="rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]"> <div class="rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div> <div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div>
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]" @change="authTypeChange"> <a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]" @change="authTypeChange">
<a-radio value="none">No Auth</a-radio> <a-radio :value="RequestAuthType.NONE">No Auth</a-radio>
<a-radio value="basic">Basic Auth</a-radio> <a-radio :value="RequestAuthType.BASIC">Basic Auth</a-radio>
<a-radio value="digest">Digest Auth</a-radio> <a-radio :value="RequestAuthType.DIGEST">Digest Auth</a-radio>
</a-radio-group> </a-radio-group>
<a-form v-if="authForm.authType !== 'none'" ref="authFormRef" :model="authForm" layout="vertical"> <a-form v-if="authForm.authType !== 'NONE'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item :label="t('apiTestDebug.account')"> <a-form-item :label="t('apiTestDebug.username')">
<a-input <a-input
v-model:model-value="authForm.account" v-model:model-value="authForm.username"
:placeholder="t('apiTestDebug.commonPlaceholder')" :placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]" class="w-[450px]"
:max-length="255" :max-length="255"
@ -34,17 +34,15 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
interface AuthForm { import { ExecuteAuthConfig } from '@/models/apiTest/debug';
authType: string; import { RequestAuthType } from '@/enums/apiEnum';
account: string;
password: string;
}
const props = defineProps<{ const props = defineProps<{
params: AuthForm; params: ExecuteAuthConfig;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', val: AuthForm): void; (e: 'update:params', val: ExecuteAuthConfig): void;
(e: 'change', val: AuthForm): void; (e: 'change', val: ExecuteAuthConfig): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -61,7 +59,7 @@
function authTypeChange(val: string | number | boolean) { function authTypeChange(val: string | number | boolean) {
if (val === 'none') { if (val === 'none') {
authForm.value.account = ''; authForm.value.username = '';
authForm.value.password = ''; authForm.value.password = '';
} }
} }

View File

@ -47,7 +47,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestContentTypeEnum } from '@/enums/apiEnum'; import { RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
params: Record<string, any>[]; params: Record<string, any>[];
@ -90,7 +90,7 @@
name: name.trim(), name: name.trim(),
value: value?.trim(), value: value?.trim(),
required: false, required: false,
type: 'string', type: RequestParamsType.STRING,
min: undefined, min: undefined,
max: undefined, max: undefined,
contentType: RequestContentTypeEnum.TEXT, contentType: RequestContentTypeEnum.TEXT,

View File

@ -1,33 +1,42 @@
<template> <template>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('apiTestDebug.body') }}</div> <batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<div class="flex items-center gap-[16px]"> <a-radio-group v-model:model-value="bodyType" type="button" size="small" @change="formatChange">
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" /> <a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ requestBodyTypeMap[item] }}</a-radio>
<a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange"> </a-radio-group>
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ item }}</a-radio>
</a-radio-group>
</div>
</div> </div>
<div <div
v-if="format === RequestBodyFormat.NONE" v-if="bodyType === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]" class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
> >
{{ t('apiTestDebug.noneBody') }} {{ t('apiTestDebug.noneBody') }}
</div> </div>
<paramTable <paramTable
v-else-if="showParamTable" v-else-if="bodyType === RequestBodyFormat.FORM_DATA"
v-model:params="currentTableParams" v-model:params="currentTableParams"
:scroll="{ minWidth: 1160 }" :scroll="{ minWidth: 1160 }"
:columns="columns" :columns="columns"
:height-used="heightUsed" :height-used="heightUsed"
@change="handleParamTableChange" @change="handleParamTableChange"
/> />
<div v-else-if="format === RequestBodyFormat.BINARY"> <paramTable
v-else-if="bodyType === RequestBodyFormat.WWW_FORM"
v-model:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
:height-used="heightUsed"
@change="handleParamTableChange"
/>
<div v-else-if="bodyType === RequestBodyFormat.BINARY">
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]"> <div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input v-model:model-value="innerParams.binaryDesc" :placeholder="t('common.desc')" :max-length="255" /> <a-input
v-model:model-value="innerParams.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
/>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small" type="line"></a-switch> <!-- <a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small" type="line"></a-switch> -->
<span>{{ t('apiTestDebug.sendAsMainText') }}</span> <span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right"> <a-tooltip position="right">
<template #content> <template #content>
@ -72,23 +81,14 @@
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue'; import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue'; import batchAddKeyVal from './batchAddKeyVal.vue';
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat } from '@/enums/apiEnum'; import { ExecuteBody } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
export interface BodyParams {
format: RequestBodyFormat;
formData: Record<string, any>[];
formUrlEncode: Record<string, any>[];
json: string;
xml: string;
binary: string;
binaryDesc: string;
binarySend: boolean;
raw: string;
}
const props = defineProps<{ const props = defineProps<{
params: BodyParams; params: ExecuteBody;
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
secondBoxHeight: number; secondBoxHeight: number;
}>(); }>();
@ -100,8 +100,9 @@
const { t } = useI18n(); const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit); const innerParams = useVModel(props, 'params', emit);
const bodyType = ref(RequestBodyFormat.NONE);
const columns: ParamTableColumn[] = [ const columns = computed<ParamTableColumn[]>(() => [
{ {
title: 'apiTestDebug.paramName', title: 'apiTestDebug.paramName',
dataIndex: 'name', dataIndex: 'name',
@ -112,32 +113,10 @@
dataIndex: 'type', dataIndex: 'type',
slotName: 'type', slotName: 'type',
hasRequired: true, hasRequired: true,
typeOptions: [ typeOptions: Object.keys(RequestParamsType).map((key) => ({
{ label: RequestParamsType[key],
label: 'string', value: key,
value: 'string', })),
},
{
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
{
label: 'json',
value: 'json',
},
{
label: 'file',
value: 'file',
},
],
width: 120, width: 120,
}, },
{ {
@ -168,9 +147,10 @@
title: '', title: '',
slotName: 'operation', slotName: 'operation',
fixed: 'right', fixed: 'right',
width: 50, format: bodyType.value,
width: bodyType.value === RequestBodyFormat.FORM_DATA ? 90 : 50,
}, },
]; ]);
const heightUsed = ref<number | undefined>(undefined); const heightUsed = ref<number | undefined>(undefined);
@ -196,61 +176,60 @@
} }
); );
const format = ref(RequestBodyFormat.NONE);
const showParamTable = computed(() => { const showParamTable = computed(() => {
// FORM_DATAX_WWW_FORM_URLENCODED // FORM_DATAX_WWW_FORM_URLENCODED
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value); return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(bodyType.value);
}); });
// //
const currentTableParams = computed({ const currentTableParams = computed({
get() { get() {
if (format.value === RequestBodyFormat.FORM_DATA) { if (bodyType.value === RequestBodyFormat.FORM_DATA) {
return innerParams.value.formData; return innerParams.value.formDataBody.formValues;
} }
return innerParams.value.formUrlEncode; return innerParams.value.wwwFormBody.formValues;
}, },
set(val) { set(val) {
if (format.value === RequestBodyFormat.FORM_DATA) { if (bodyType.value === RequestBodyFormat.FORM_DATA) {
innerParams.value.formData = val; innerParams.value.formDataBody.formValues = val;
} else { } else {
innerParams.value.formUrlEncode = val; innerParams.value.wwwFormBody.formValues = val;
} }
}, },
}); });
// //
const currentBodyCode = computed({ const currentBodyCode = computed({
get() { get() {
if (format.value === RequestBodyFormat.JSON) { if (bodyType.value === RequestBodyFormat.JSON) {
return innerParams.value.json; return innerParams.value.jsonBody.jsonValue;
} }
if (format.value === RequestBodyFormat.XML) { if (bodyType.value === RequestBodyFormat.XML) {
return innerParams.value.xml; return innerParams.value.xmlBody.value;
} }
return innerParams.value.raw; return innerParams.value.rawBody.value;
}, },
set(val) { set(val) {
if (format.value === RequestBodyFormat.JSON) { if (bodyType.value === RequestBodyFormat.JSON) {
innerParams.value.json = val; innerParams.value.jsonBody.jsonValue = val;
} else if (format.value === RequestBodyFormat.XML) { } else if (bodyType.value === RequestBodyFormat.XML) {
innerParams.value.xml = val; innerParams.value.xmlBody.value = val;
} else { } else {
innerParams.value.raw = val; innerParams.value.rawBody.value = val;
} }
}, },
}); });
// //
const currentCodeLanguage = computed(() => { const currentCodeLanguage = computed(() => {
if (format.value === RequestBodyFormat.JSON) { if (bodyType.value === RequestBodyFormat.JSON) {
return LanguageEnum.JSON; return LanguageEnum.JSON;
} }
if (format.value === RequestBodyFormat.XML) { if (bodyType.value === RequestBodyFormat.XML) {
return LanguageEnum.XML; return LanguageEnum.XML;
} }
return LanguageEnum.PLAINTEXT; return LanguageEnum.PLAINTEXT;
}); });
function formatChange() { function formatChange() {
console.log('formatChange', format.value); console.log('formatChange', bodyType.value);
} }
/** /**

View File

@ -5,6 +5,7 @@
</div> </div>
<paramTable <paramTable
v-model:params="innerParams" v-model:params="innerParams"
v-model:selected-keys="selectedKeys"
:columns="columns" :columns="columns"
:height-used="heightUsed" :height-used="heightUsed"
:scroll="scroll" :scroll="scroll"
@ -21,19 +22,24 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { EnableKeyValueParam } from '@/models/apiTest/debug';
const props = defineProps<{ const props = defineProps<{
params: any[]; selectedKeys?: string[];
params: EnableKeyValueParam[];
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
secondBoxHeight: number; secondBoxHeight: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', value: any[]): void; (e: 'update:selectedKeys', value: string[]): void;
(e: 'update:params', value: EnableKeyValueParam[]): void;
(e: 'change'): void; // (e: 'change'): void; //
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit); const innerParams = useVModel(props, 'params', emit);
const selectedKeys = useVModel(props, 'selectedKeys', emit);
const columns: ParamTableColumn[] = [ const columns: ParamTableColumn[] = [
{ {

View File

@ -11,7 +11,7 @@
@more-action-select="handleMoreActionSelect" @more-action-select="handleMoreActionSelect"
> >
<template #label="{ tab }"> <template #label="{ tab }">
<apiMethodName :method="tab.method" class="mr-[4px]" /> <apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
{{ tab.label }} {{ tab.label }}
</template> </template>
</MsEditableTab> </MsEditableTab>
@ -20,12 +20,13 @@
<div class="mb-[4px] flex items-center justify-between"> <div class="mb-[4px] flex items-center justify-between">
<div class="flex flex-1"> <div class="flex flex-1">
<a-select <a-select
v-model:model-value="activeDebug.moduleProtocol" v-model:model-value="activeDebug.protocol"
:options="moduleProtocolOptions" :options="protocolOptions"
:loading="protocolLoading"
class="mr-[4px] w-[90px]" class="mr-[4px] w-[90px]"
@change="handleActiveDebugChange" @change="(val) => handleActiveDebugProtocolChange(val as string)"
/> />
<a-input-group class="flex-1"> <a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect <apiMethodSelect
v-model:model-value="activeDebug.method" v-model:model-value="activeDebug.method"
class="w-[140px]" class="w-[140px]"
@ -40,7 +41,12 @@
</a-input-group> </a-input-group>
</div> </div>
<div class="ml-[16px]"> <div class="ml-[16px]">
<a-dropdown-button class="exec-btn"> <a-dropdown-button
:button-props="{ loading: executeLoading }"
:disabled="executeLoading"
class="exec-btn"
@click="execute"
>
{{ t('apiTestDebug.serverExec') }} {{ t('apiTestDebug.serverExec') }}
<template #icon> <template #icon>
<icon-down /> <icon-down />
@ -49,7 +55,7 @@
<a-doption>{{ t('apiTestDebug.localExec') }}</a-doption> <a-doption>{{ t('apiTestDebug.localExec') }}</a-doption>
</template> </template>
</a-dropdown-button> </a-dropdown-button>
<a-button type="secondary"> <a-button type="secondary" @click="handleSaveShortcut">
<div class="flex items-center"> <div class="flex items-center">
{{ t('common.save') }} {{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div> <div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div>
@ -69,62 +75,75 @@
@expand-change="handleExpandChange" @expand-change="handleExpandChange"
> >
<template #first> <template #first>
<div :class="`h-full min-w-[800px] px-[24px] pb-[16px] ${activeLayout === 'horizontal' ? ' pr-[16px]' : ''}`"> <div
<a-tabs v-model:active-key="activeDebug.activeTab" class="no-content"> :class="`flex h-full min-w-[800px] flex-col px-[24px] pb-[16px] ${
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" /> activeLayout === 'horizontal' ? ' pr-[16px]' : ''
</a-tabs> }`"
<a-divider margin="0" class="!mb-[16px]"></a-divider> >
<debugHeader <div>
v-if="activeDebug.activeTab === RequestComposition.HEADER" <a-tabs v-model:active-key="activeDebug.activeTab" class="no-content">
v-model:params="activeDebug.headerParams" <a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
:layout="activeLayout" </a-tabs>
:second-box-height="secondBoxHeight" <a-divider margin="0" class="!mb-[16px]"></a-divider>
@change="handleActiveDebugChange" </div>
/> <div class="tab-pane-container">
<debugBody <template v-if="isInitPluginForm || activeDebug.activeTab === RequestComposition.PLUGIN">
v-else-if="activeDebug.activeTab === RequestComposition.BODY" <a-spin v-show="activeDebug.activeTab === RequestComposition.PLUGIN" :loading="pluginLoading">
v-model:params="activeDebug.bodyParams" <MsFormCreate v-model:api="fApi" :rule="currentPluginScript" :option="options" />
:layout="activeLayout" </a-spin>
:second-box-height="secondBoxHeight" </template>
@change="handleActiveDebugChange" <debugHeader
/> v-if="activeDebug.activeTab === RequestComposition.HEADER"
<debugQuery v-model:params="activeDebug.headers"
v-else-if="activeDebug.activeTab === RequestComposition.QUERY" :layout="activeLayout"
v-model:params="activeDebug.queryParams" :second-box-height="secondBoxHeight"
:layout="activeLayout" @change="handleActiveDebugChange"
:second-box-height="secondBoxHeight" />
@change="handleActiveDebugChange" <debugBody
/> v-else-if="activeDebug.activeTab === RequestComposition.BODY"
<debugRest v-model:params="activeDebug.body"
v-else-if="activeDebug.activeTab === RequestComposition.REST" :layout="activeLayout"
v-model:params="activeDebug.restParams" :second-box-height="secondBoxHeight"
:layout="activeLayout" @change="handleActiveDebugChange"
:second-box-height="secondBoxHeight" />
@change="handleActiveDebugChange" <debugQuery
/> v-else-if="activeDebug.activeTab === RequestComposition.QUERY"
<precondition v-model:params="activeDebug.query"
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION" :layout="activeLayout"
v-model:params="activeDebug.preconditions" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<postcondition <debugRest
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION" v-else-if="activeDebug.activeTab === RequestComposition.REST"
v-model:params="activeDebug.postConditions" v-model:params="activeDebug.rest"
:response="activeDebug.response.body" :layout="activeLayout"
:layout="activeLayout" :second-box-height="secondBoxHeight"
:second-box-height="secondBoxHeight" @change="handleActiveDebugChange"
@change="handleActiveDebugChange" />
/> <precondition
<debugAuth v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-else-if="activeDebug.activeTab === RequestComposition.AUTH" v-model:params="activeDebug.children[0].preProcessorConfig.processors"
v-model:params="activeDebug.authParams" @change="handleActiveDebugChange"
@change="handleActiveDebugChange" />
/> <postcondition
<debugSetting v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-else-if="activeDebug.activeTab === RequestComposition.SETTING" v-model:params="activeDebug.children[0].postProcessorConfig.processors"
v-model:params="activeDebug.setting" :response="activeDebug.response.requestResults[0]?.responseResult.body"
@change="handleActiveDebugChange" :layout="activeLayout"
/> :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="activeDebug.activeTab === RequestComposition.AUTH"
v-model:params="activeDebug.authConfig"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="activeDebug.activeTab === RequestComposition.SETTING"
v-model:params="activeDebug.otherConfig"
@change="handleActiveDebugChange"
/>
</div>
</div> </div>
</template> </template>
<template #second> <template #second>
@ -147,6 +166,7 @@
title-align="start" title-align="start"
body-class="!p-0" body-class="!p-0"
@before-ok="handleSave" @before-ok="handleSave"
@cancel="handleCancel"
> >
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical"> <a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
<a-form-item <a-form-item
@ -158,16 +178,17 @@
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" /> <a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="url" v-if="isHttpProtocol"
field="path"
:label="t('apiTestDebug.requestUrl')" :label="t('apiTestDebug.requestUrl')"
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]" :rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end" asterisk-position="end"
> >
<a-input v-model:model-value="saveModalForm.url" :placeholder="t('apiTestDebug.commonPlaceholder')" /> <a-input v-model:model-value="saveModalForm.path" :placeholder="t('apiTestDebug.commonPlaceholder')" />
</a-form-item> </a-form-item>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0"> <a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select <a-tree-select
v-model:modelValue="saveModalForm.module" v-model:modelValue="saveModalForm.moduleId"
:data="props.moduleTree" :data="props.moduleTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
allow-search allow-search
@ -178,150 +199,163 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es'; import { cloneDeep, debounce } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue'; import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import debugAuth from './auth.vue'; import debugAuth from './auth.vue';
import debugBody, { BodyParams } from './body.vue';
import debugHeader from './header.vue';
import postcondition from './postcondition.vue'; import postcondition from './postcondition.vue';
import precondition from './precondition.vue'; import precondition from './precondition.vue';
import debugQuery from './query.vue';
import response from './response.vue'; import response from './response.vue';
import debugRest from './rest.vue';
import debugSetting from './setting.vue'; import debugSetting from './setting.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import { addDebug, executeDebug } from '@/api/modules/api-test/debug';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event'; import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { ExecuteBody, ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { RequestBodyFormat, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
// Http
const debugHeader = defineAsyncComponent(() => import('./header.vue'));
const debugBody = defineAsyncComponent(() => import('./body.vue'));
const debugQuery = defineAsyncComponent(() => import('./query.vue'));
const debugRest = defineAsyncComponent(() => import('./rest.vue'));
export type DebugTabParam = ExecuteHTTPRequestFullParams & TabItem & Record<string, any>;
const props = defineProps<{ const props = defineProps<{
module: string; // module: string; //
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const initDefaultId = `debug-${Date.now()}`; const initDefaultId = `debug-${Date.now()}`;
const activeRequestTab = ref<string | number>(initDefaultId); const activeRequestTab = ref<string | number>(initDefaultId);
const defaultBodyParams: BodyParams = { const defaultBodyParams: ExecuteBody = {
format: RequestBodyFormat.NONE, bodyType: RequestBodyFormat.NONE,
formData: [], formDataBody: {
formUrlEncode: [], formValues: [],
json: '', },
xml: '', wwwFormBody: {
binary: '', formValues: [],
binaryDesc: '', },
binarySend: false, jsonBody: {
raw: '', jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
}; };
const defaultDebugParams = { const defaultDebugParams: DebugTabParam = {
id: initDefaultId, id: initDefaultId,
module: 'root', moduleId: 'root',
moduleProtocol: 'http', protocol: 'HTTP',
url: '', url: '',
activeTab: RequestComposition.HEADER, activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'), label: t('apiTestDebug.newApi'),
closable: true, closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSaved: false, unSaved: false,
headerParams: [], headers: [],
bodyParams: cloneDeep(defaultBodyParams), body: cloneDeep(defaultBodyParams),
queryParams: [], query: [],
restParams: [], rest: [],
authParams: { polymorphicName: '',
authType: 'none', name: '',
account: '', path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: 'NONE',
username: '',
password: '', password: '',
}, },
preconditions: [], children: [
postConditions: [], {
setting: { polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000, connectTimeout: 60000,
responseTimeout: 60000, responseTimeout: 60000,
certificateAlias: '', certificateAlias: '',
redirect: 'follow', followRedirects: false,
autoRedirects: false,
}, },
responseActiveTab: ResponseComposition.BODY, responseActiveTab: ResponseComposition.BODY,
response: { response: {
status: 200, requestResults: [
headers: [], {
timing: 12938, body: '',
size: 8734, responseResult: {
env: 'Mock', body: '',
resource: '66', contentType: '',
timingInfo: { headers: '',
ready: 10, dnsLookupTime: 0,
socketInit: 50, downloadTime: 0,
dnsQuery: 20, latency: 0,
tcpHandshake: 80, responseCode: 0,
sslHandshake: 40, responseTime: 0,
waitingTTFB: 30, responseSize: 0,
downloadContent: 10, socketInitTime: 0,
deal: 10, tcpHandshakeTime: 0,
total: 250, transferStartTime: 0,
}, },
extract: { },
a: 'asdasd', ],
b: 'asdasdasd43f43', console: '',
},
console: `GET https://qa-release.fit2cloud.com/test`,
content: `请求地址:
https://qa-release.fit2cloud.com/test
请求头:
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: qa-release.fit2cloud.com
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Body:
POST https://qa-release.fit2cloud.com/test
POST data:
[no cookies]
`,
header: `HTTP/ 1.1 200 OK
Content-Length: 2381
Content-Type: text/html
Server: bfe
Date: Wed, 13 Dec 2023 08:53:25 GMTHTTP/ 1.1 200 OK
Content-Length: 2381
Content-Type: text/html
Server: bfe
Date: Wed, 13 Dec 2023 08:53:25 GMT`,
body: `<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<connectionStrings>
<add name="MyDB"
connectionString="value for the deployed Web.config file"
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
</connectionStrings>
<a>哈哈哈哈哈哈哈</a>
<system.web>
<customErrors defaultRedirect="GenericError.htm"
mode="RemoteOnly" xdt:Transform="Replace">
<error statusCode="500" redirect="InternalError.htm"/>
</customErrors>
</system.web>
</configuration>`,
}, // }, //
}; };
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]); const debugTabs = ref<DebugTabParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<TabItem>(debugTabs.value[0]); const activeDebug = ref<DebugTabParam>(debugTabs.value[0]);
const isHttpProtocol = computed(() => activeDebug.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); //
watch(
() => activeDebug.value.protocol,
(val) => {
if (val !== 'HTTP') {
isInitPluginForm.value = true;
}
},
{
immediate: true,
}
);
function setActiveDebug(item: TabItem) { function setActiveDebug(item: TabItem) {
activeDebug.value = item; activeDebug.value = item as DebugTabParam;
} }
function handleActiveDebugChange() { function handleActiveDebugChange() {
@ -332,7 +366,7 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
const id = `debug-${Date.now()}`; const id = `debug-${Date.now()}`;
debugTabs.value.push({ debugTabs.value.push({
...cloneDeep(defaultDebugParams), ...cloneDeep(defaultDebugParams),
module: props.module, moduleId: props.module,
id, id,
...defaultProps, ...defaultProps,
}); });
@ -365,7 +399,21 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
} }
} }
const contentTabList = [ // tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{ {
value: RequestComposition.HEADER, value: RequestComposition.HEADER,
label: t('apiTestDebug.header'), label: t('apiTestDebug.header'),
@ -403,13 +451,82 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
label: t('apiTestDebug.setting'), label: t('apiTestDebug.setting'),
}, },
]; ];
// tab
const contentTabList = computed(() =>
isHttpProtocol.value
? httpContentTabList
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]
);
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
const moduleProtocolOptions = ref([ async function initProtocolList() {
{ try {
label: 'HTTP', protocolLoading.value = true;
value: 'http', const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const pluginScriptMap = ref<Record<string, any>>({}); //
const pluginLoading = ref(false);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[activeDebug.value.protocol] || []
);
async function initPluginScript() {
if (pluginScriptMap.value[activeDebug.value.protocol] !== undefined) {
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(
protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.pluginId || ''
);
pluginScriptMap.value[activeDebug.value.protocol] = res.script;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
function handleActiveDebugProtocolChange(val: string) {
if (val !== 'HTTP') {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
initPluginScript();
} else {
activeDebug.value.activeTab = RequestComposition.HEADER;
}
handleActiveDebugChange();
}
const fApi = ref();
const options = {
form: {
layout: 'vertical',
labelPosition: 'right',
size: 'small',
labelWidth: '00px',
hideRequiredAsterisk: false,
showMessage: true,
inlineMessage: false,
scrollToFirstError: true,
}, },
]); submitBtn: false,
resetBtn: false,
};
const splitBoxSize = ref<string | number>(0.6); const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical'); const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
@ -455,11 +572,120 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
splitBoxRef.value?.expand(0.6); splitBoxRef.value?.expand(0.6);
} }
const executeLoading = ref(false);
const reportId = ref('');
const websocket = ref<WebSocket>();
function debugSocket() {
websocket.value = getSocket(reportId.value);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
activeDebug.value.response = data.taskResult;
executeLoading.value = false;
}
});
websocket.value.addEventListener('close', (event) => {
console.log('关闭:', event);
});
websocket.value.addEventListener('error', (event) => {
console.error('错误:', event);
});
}
function makeRequestParams() {
const polymorphicName = protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.polymorphicName; //
let requestParams;
if (isHttpProtocol.value) {
requestParams = {
authConfig: activeDebug.value.authConfig,
body: { ...activeDebug.value.body, binaryBody: undefined },
headers: activeDebug.value.headers,
method: activeDebug.value.method,
otherConfig: activeDebug.value.otherConfig,
path: activeDebug.value.url,
query: activeDebug.value.query,
rest: activeDebug.value.rest,
url: activeDebug.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value.form,
polymorphicName,
};
}
reportId.value = getGenerateId();
debugSocket(); // websocket
return {
id: activeDebug.value.id.toString(),
reportId: reportId.value,
environmentId: '',
tempFileIds: [],
request: {
...requestParams,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
// TODO:
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
},
projectId: appStore.currentProjectId,
};
}
async function execute() {
if (isHttpProtocol.value) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
//
fApi.value?.validate(async (valid) => {
if (valid === true) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
});
}
}
const saveModalVisible = ref(false); const saveModalVisible = ref(false);
const saveModalForm = ref({ const saveModalForm = ref({
name: '', name: '',
url: activeDebug.value.url, path: activeDebug.value.url || '',
module: activeDebug.value.module, moduleId: activeDebug.value.module,
}); });
const saveModalFormRef = ref<FormInstance>(); const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false); const saveLoading = ref(false);
@ -473,22 +699,46 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
} }
); );
function handleSaveShortcut() { async function handleSaveShortcut() {
saveModalForm.value = { try {
name: '', if (!isHttpProtocol.value) {
url: activeDebug.value.url, //
module: activeDebug.value.module, await fApi.value?.validate();
}; }
saveModalVisible.value = true; saveModalForm.value = {
name: '',
path: activeDebug.value.url || '',
moduleId: activeDebug.value.module,
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
} }
function handleSave(done: (closed: boolean) => void) { function handleCancel() {
saveModalFormRef.value?.resetFields();
}
async function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => { saveModalFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
try { try {
saveLoading.value = true; saveLoading.value = true;
// eslint-disable-next-line no-promise-executor-return await addDebug({
await new Promise((resolve) => setTimeout(resolve, 2000)); ...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
saveLoading.value = false; saveLoading.value = false;
saveModalVisible.value = false; saveModalVisible.value = false;
done(true); done(true);
@ -497,12 +747,15 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
} catch (error) { } catch (error) {
saveLoading.value = false; saveLoading.value = false;
} }
} else {
done(false);
} }
}); });
done(false);
} }
onBeforeMount(() => {
initProtocolList();
});
onMounted(() => { onMounted(() => {
registerCatchSaveShortcut(handleSaveShortcut); registerCatchSaveShortcut(handleSaveShortcut);
}); });
@ -527,6 +780,10 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
.btn-base-primary-disabled(); .btn-base-primary-disabled();
} }
} }
.tab-pane-container {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
}
:deep(.no-content) { :deep(.no-content) {
.arco-tabs-content { .arco-tabs-content {
display: none; display: none;

View File

@ -1,13 +1,13 @@
<template> <template>
<condition <condition
v-model:list="postConditions" v-model:list="postConditions"
:condition-types="['script', 'sql', 'extract']" :condition-types="['SCRIPT']"
add-text="apiTestDebug.postCondition" add-text="apiTestDebug.postCondition"
:response="props.response" :response="props.response"
:height-used="heightUsed" :height-used="heightUsed"
@change="emit('change')" @change="emit('change')"
> >
<template #titleRight> <!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPostCondition" size="small" type="line"></a-switch> <a-switch v-model:model-value="openGlobalPostCondition" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPostCondition') }}</div> <div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPostCondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPostConditionTip')" position="left"> <a-tooltip :content="t('apiTestDebug.openGlobalPostConditionTip')" position="left">
@ -16,7 +16,7 @@
size="16" size="16"
/> />
</a-tooltip> </a-tooltip>
</template> </template> -->
</condition> </condition>
</template> </template>
@ -25,22 +25,24 @@
import condition from '@/views/api-test/components/condition/index.vue'; import condition from '@/views/api-test/components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{ const props = defineProps<{
params: any[]; params: ExecuteConditionProcessor[];
secondBoxHeight?: number; secondBoxHeight?: number;
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
response?: string; // response?: string; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', params: any[]): void; (e: 'update:params', params: ExecuteConditionProcessor[]): void;
(e: 'change'): void; (e: 'change'): void;
}>(); }>();
const { t } = useI18n(); // const { t } = useI18n();
// //
const openGlobalPostCondition = ref(false); // const openGlobalPostCondition = ref(false);
const postConditions = useVModel(props, 'params', emit); const postConditions = useVModel(props, 'params', emit);
const heightUsed = computed(() => { const heightUsed = computed(() => {
if (props.layout === 'horizontal') { if (props.layout === 'horizontal') {

View File

@ -1,11 +1,11 @@
<template> <template>
<condition <condition
v-model:list="preconditions" v-model:list="preconditions"
:condition-types="['script', 'sql', 'waitTime']" :condition-types="['SCRIPT', 'TIME_WAITING']"
add-text="apiTestDebug.precondition" add-text="apiTestDebug.precondition"
@change="emit('change')" @change="emit('change')"
> >
<template #titleRight> <!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPrecondition" size="small" type="line"></a-switch> <a-switch v-model:model-value="openGlobalPrecondition" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPrecondition') }}</div> <div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPrecondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="left"> <a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="left">
@ -14,7 +14,7 @@
size="16" size="16"
/> />
</a-tooltip> </a-tooltip>
</template> </template> -->
</condition> </condition>
</template> </template>
@ -23,19 +23,21 @@
import condition from '@/views/api-test/components/condition/index.vue'; import condition from '@/views/api-test/components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{ const props = defineProps<{
params: any[]; params: ExecuteConditionProcessor[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', params: any[]): void; (e: 'update:params', params: ExecuteConditionProcessor[]): void;
(e: 'change'): void; (e: 'change'): void;
}>(); }>();
const { t } = useI18n(); // const { t } = useI18n();
// //
const openGlobalPrecondition = ref(false); // const openGlobalPrecondition = ref(false);
const preconditions = useVModel(props, 'params', emit); const preconditions = useVModel(props, 'params', emit);
</script> </script>

View File

@ -28,8 +28,11 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExecuteRequestCommonParam } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
params: any[]; params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
secondBoxHeight: number; secondBoxHeight: number;
}>(); }>();
@ -53,24 +56,12 @@
dataIndex: 'type', dataIndex: 'type',
slotName: 'type', slotName: 'type',
hasRequired: true, hasRequired: true,
typeOptions: [ typeOptions: Object.keys(RequestParamsType)
{ .filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
label: 'string', .map((key) => ({
value: 'string', label: RequestParamsType[key],
}, value: key,
{ })),
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
],
width: 120, width: 120,
}, },
{ {
@ -100,8 +91,7 @@
title: '', title: '',
slotName: 'operation', slotName: 'operation',
fixed: 'right', fixed: 'right',
format: 'query', width: 50,
width: 80,
}, },
]; ];

View File

@ -32,36 +32,51 @@
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio> <a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div v-if="props.response.status" class="flex items-center justify-between gap-[24px]"> <div
v-if="props.response.requestResults[0]?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px]"
>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div> <div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
</div>
<template #content> <template #content>
<div class="flex items-center gap-[8px] text-[14px]"> <div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div> <div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
</div>
</div> </div>
</template> </template>
</a-popover> </a-popover>
<a-popover position="left" content-class="w-[400px]"> <a-popover position="left" content-class="w-[400px]">
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div> <div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
</div>
<template #content> <template #content>
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]"> <div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
<div class="text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div> <div class="text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
</div>
</div> </div>
<responseTimeLine :response-timing="$props.response.timingInfo" /> <responseTimeLine :response-timing="timingInfo" />
</template> </template>
</a-popover> </a-popover>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div> <div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
</div>
<template #content> <template #content>
<div class="flex items-center gap-[8px] text-[14px]"> <div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div> <div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
</div>
</div> </div>
</template> </template>
</a-popover> </a-popover>
<a-popover position="left" content-class="response-popover-content"> <!-- <a-popover position="left" content-class="response-popover-content">
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div> <div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
<template #content> <template #content>
<div class="flex items-center gap-[8px] text-[14px]"> <div class="flex items-center gap-[8px] text-[14px]">
@ -78,7 +93,7 @@
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div> <div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
</div> </div>
</template> </template>
</a-popover> </a-popover> -->
</div> </div>
</div> </div>
<div class="h-[calc(100%-42px)] px-[16px] pb-[16px]"> <div class="h-[calc(100%-42px)] px-[16px] pb-[16px]">
@ -88,8 +103,9 @@
<div class="response-container"> <div class="response-container">
<MsCodeEditor <MsCodeEditor
v-if="activeTab === ResponseComposition.BODY" v-if="activeTab === ResponseComposition.BODY"
:model-value="props.response.body" ref="responseEditorRef"
language="json" :model-value="props.response.requestResults[0]?.responseResult?.body || ''"
:language="responseLanguage"
theme="vs" theme="vs"
height="100%" height="100%"
:languages="['json', 'html', 'xml', 'plaintext']" :languages="['json', 'html', 'xml', 'plaintext']"
@ -143,21 +159,28 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ResponseTiming } from '@/models/apiTest/debug';
import { ResponseComposition } from '@/enums/apiEnum'; import { ResponseComposition } from '@/enums/apiEnum';
export interface Response { export interface Response {
status: number; requestResults: {
timing: number; body: string;
size: number; responseResult: {
env: string; body: string;
resource: string; contentType: string;
body: string; headers: string;
header: string; dnsLookupTime: number;
content: string; downloadTime: number;
latency: number;
responseCode: number;
responseTime: number;
responseSize: number;
socketInitTime: number;
sslHandshakeTime: number;
tcpHandshakeTime: number;
transferStartTime: number;
};
}[]; //
console: string; console: string;
extract: Record<string, any>;
timingInfo: ResponseTiming;
} }
const props = defineProps<{ const props = defineProps<{
@ -177,6 +200,66 @@
const innerLayout = useVModel(props, 'activeLayout', emit); const innerLayout = useVModel(props, 'activeLayout', emit);
const activeTab = useVModel(props, 'activeTab', emit); const activeTab = useVModel(props, 'activeTab', emit);
//
const timingInfo = computed(() => {
const {
dnsLookupTime,
downloadTime,
latency,
responseTime,
socketInitTime,
sslHandshakeTime,
tcpHandshakeTime,
transferStartTime,
} = props.response.requestResults[0].responseResult;
return {
dnsLookupTime,
tcpHandshakeTime,
sslHandshakeTime,
socketInitTime,
latency,
downloadTime,
transferStartTime,
responseTime,
};
});
//
const statusCodeColor = computed(() => {
const code = props.response.requestResults[0].responseResult.responseCode;
if (code >= 200 && code < 300) {
return 'rgb(var(--success-7)';
}
if (code >= 300 && code < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
});
//
const responseLanguage = computed(() => {
const { contentType } = props.response.requestResults[0].responseResult;
if (contentType.includes('json')) {
return 'json';
}
if (contentType.includes('html')) {
return 'html';
}
if (contentType.includes('xml')) {
return 'xml';
}
return 'plaintext';
});
const responseEditorRef = ref<InstanceType<typeof MsCodeEditor>>();
watch(
() => props.response,
(obj) => {
if (obj.requestResults[0].responseResult.body.trim() !== '') {
nextTick(() => {
responseEditorRef.value?.format();
});
}
}
);
const responseTabList = [ const responseTabList = [
{ {
@ -195,21 +278,21 @@
label: t('apiTestDebug.console'), label: t('apiTestDebug.console'),
value: ResponseComposition.CONSOLE, value: ResponseComposition.CONSOLE,
}, },
{ // {
label: t('apiTestDebug.extract'), // label: t('apiTestDebug.extract'),
value: ResponseComposition.EXTRACT, // value: ResponseComposition.EXTRACT,
}, // },
{ // {
label: t('apiTestDebug.assertion'), // label: t('apiTestDebug.assertion'),
value: ResponseComposition.ASSERTION, // value: ResponseComposition.ASSERTION,
}, // },
]; ];
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
function copyScript() { function copyScript() {
if (isSupported) { if (isSupported) {
copy(props.response.body); copy(props.response.requestResults[0].responseResult.body);
Message.success(t('common.copySuccess')); Message.success(t('common.copySuccess'));
} else { } else {
Message.warning(t('apiTestDebug.copyNotSupport')); Message.warning(t('apiTestDebug.copyNotSupport'));
@ -219,15 +302,15 @@
function getResponsePreContent(type: keyof typeof ResponseComposition) { function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) { switch (type) {
case ResponseComposition.HEADER: case ResponseComposition.HEADER:
return props.response.header.trim(); return props.response.requestResults[0].responseResult.headers.trim();
case ResponseComposition.REAL_REQUEST: case ResponseComposition.REAL_REQUEST:
return props.response.content.trim(); return props.response.requestResults[0].body.trim();
case ResponseComposition.CONSOLE: case ResponseComposition.CONSOLE:
return props.response.console.trim(); return props.response.console.trim();
case ResponseComposition.EXTRACT: // case ResponseComposition.EXTRACT:
return Object.keys(props.response.extract) // return Object.keys(props.response.extract)
.map((e) => `${e}: ${props.response.extract[e]}`) // .map((e) => `${e}: ${props.response.extract[e]}`)
.join('\n'); // .join('\n');
default: default:
return ''; return '';
} }

View File

@ -28,8 +28,11 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExecuteRequestCommonParam } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
params: any[]; params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical'; layout: 'horizontal' | 'vertical';
secondBoxHeight: number; secondBoxHeight: number;
}>(); }>();
@ -53,24 +56,12 @@
dataIndex: 'type', dataIndex: 'type',
slotName: 'type', slotName: 'type',
hasRequired: true, hasRequired: true,
typeOptions: [ typeOptions: Object.keys(RequestParamsType)
{ .filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
label: 'string', .map((key) => ({
value: 'string', label: RequestParamsType[key],
}, value: key,
{ })),
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
],
width: 120, width: 120,
}, },
{ {
@ -100,8 +91,7 @@
title: '', title: '',
slotName: 'operation', slotName: 'operation',
fixed: 'right', fixed: 'right',
format: 'query', width: 50,
width: 80,
}, },
]; ];

View File

@ -43,7 +43,7 @@
/> />
</a-form-item> </a-form-item>
<a-form-item :label="t('apiTestDebug.redirect')"> <a-form-item :label="t('apiTestDebug.redirect')">
<a-radio-group v-model:model-value="settingForm.redirect"> <a-radio-group v-model:model-value="settingForm.autoRedirects">
<a-radio value="follow">{{ t('apiTestDebug.follow') }}</a-radio> <a-radio value="follow">{{ t('apiTestDebug.follow') }}</a-radio>
<a-radio value="auto">{{ t('apiTestDebug.auto') }}</a-radio> <a-radio value="auto">{{ t('apiTestDebug.auto') }}</a-radio>
</a-radio-group> </a-radio-group>
@ -58,18 +58,14 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
interface SettingForm { import { ExecuteOtherConfig } from '@/models/apiTest/debug';
connectTimeout: number;
responseTimeout: number;
certificateAlias: string;
redirect: 'follow' | 'auto';
}
const props = defineProps<{ const props = defineProps<{
params: SettingForm; params: ExecuteOtherConfig;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', val: SettingForm): void; (e: 'update:params', val: ExecuteOtherConfig): void;
(e: 'change', val: SettingForm): void; (e: 'change', val: ExecuteOtherConfig): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -108,21 +108,24 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import popConfirm from '@/views/api-test/components/popConfirm.vue'; import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview'; import {
deleteDebugModule,
getDebugModuleCount,
getDebugModules,
moveDebugModule,
updateDebugModule,
} from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{ const props = defineProps<{
modulesCount?: Record<string, number>; //
isExpandAll?: boolean; // isExpandAll?: boolean; //
}>(); }>();
const emit = defineEmits(['init', 'change', 'newApi', 'import']); const emit = defineEmits(['init', 'change', 'newApi', 'import']);
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
@ -147,7 +150,6 @@
}); });
const activeFolder = ref<string>('all'); const activeFolder = ref<string>('all');
const allFileCount = ref(0);
const isExpandAll = ref(props.isExpandAll); const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); // const rootModulesName = ref<string[]>([]); //
@ -199,7 +201,7 @@
async function initModules() { async function initModules() {
try { try {
loading.value = true; loading.value = true;
const res = await getReviewModules(appStore.currentProjectId); const res = await getDebugModules();
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => { folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return { return {
...e, ...e,
@ -217,6 +219,26 @@
} }
} }
const modulesCount = ref<Record<string, number>>({});
const allFileCount = computed(() => modulesCount.value.all || 0);
async function initModuleCount() {
try {
const res = await getDebugModuleCount({
keyword: moduleKeyword.value,
});
modulesCount.value = res;
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: res[node.id] || 0,
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
/** /**
* 删除文件夹 * 删除文件夹
* @param node 节点信息 * @param node 节点信息
@ -233,7 +255,7 @@
maskClosable: false, maskClosable: false,
onBeforeOk: async () => { onBeforeOk: async () => {
try { try {
await deleteReviewModule(node.id); await deleteDebugModule(node.id);
Message.success(t('apiTestDebug.deleteSuccess')); Message.success(t('apiTestDebug.deleteSuccess'));
initModules(); initModules();
} catch (error) { } catch (error) {
@ -288,7 +310,7 @@
) { ) {
try { try {
loading.value = true; loading.value = true;
await moveReviewModule({ await moveDebugModule({
dragNodeId: dragNode.id as string, dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '', dropNodeId: dropNode.id || '',
dropPosition, dropPosition,
@ -300,6 +322,7 @@
} finally { } finally {
loading.value = false; loading.value = false;
initModules(); initModules();
initModuleCount();
} }
} }
@ -312,23 +335,9 @@
onBeforeMount(() => { onBeforeMount(() => {
initModules(); initModules();
initModuleCount();
}); });
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
defineExpose({ defineExpose({
initModules, initModules,
}); });

View File

@ -5,6 +5,7 @@ export default {
'apiTestDebug.serverExec': 'Server execution', 'apiTestDebug.serverExec': 'Server execution',
'apiTestDebug.localExec': 'Local execution', 'apiTestDebug.localExec': 'Local execution',
'apiTestDebug.noMatchModule': 'No matching module data yet', 'apiTestDebug.noMatchModule': 'No matching module data yet',
'apiTestDebug.pluginData': 'Request data',
'apiTestDebug.header': 'Header', 'apiTestDebug.header': 'Header',
'apiTestDebug.body': 'Body', 'apiTestDebug.body': 'Body',
'apiTestDebug.prefix': 'Precondition', 'apiTestDebug.prefix': 'Precondition',

View File

@ -5,6 +5,7 @@ export default {
'apiTestDebug.serverExec': '服务端执行', 'apiTestDebug.serverExec': '服务端执行',
'apiTestDebug.localExec': '本地执行', 'apiTestDebug.localExec': '本地执行',
'apiTestDebug.noMatchModule': '暂无匹配的模块数据', 'apiTestDebug.noMatchModule': '暂无匹配的模块数据',
'apiTestDebug.pluginData': '请求数据',
'apiTestDebug.header': '请求头', 'apiTestDebug.header': '请求头',
'apiTestDebug.body': '请求体', 'apiTestDebug.body': '请求体',
'apiTestDebug.prefix': '前置', 'apiTestDebug.prefix': '前置',

View File

@ -154,11 +154,11 @@
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const moduleProtocol = ref('http'); const moduleProtocol = ref('HTTP');
const moduleProtocolOptions = ref([ const moduleProtocolOptions = ref([
{ {
label: 'HTTP', label: 'HTTP',
value: 'http', value: 'HTTP',
}, },
]); ]);

View File

@ -9,7 +9,8 @@
const store = useProjectEnvStore(); const store = useProjectEnvStore();
const params = computed({ // TODO:
const params = computed<any>({
set: (value: any) => { set: (value: any) => {
store.currentEnvDetailInfo.config.postScript = value; store.currentEnvDetailInfo.config.postScript = value;
}, },

View File

@ -9,7 +9,8 @@
const store = useProjectEnvStore(); const store = useProjectEnvStore();
const params = computed({ // TODO:
const params = computed<any>({
set: (value: any) => { set: (value: any) => {
store.currentEnvDetailInfo.config.preScript = value; store.currentEnvDetailInfo.config.preScript = value;
}, },

View File

@ -28,17 +28,25 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["esnext", "dom"], "lib": [
"esnext",
"dom"
],
"skipLibCheck": true, // node "skipLibCheck": true, // node
"types": [ "types": [
"node",
// "vitest/globals", // "vitest/globals",
// "vite-plugin-svg-icons/client" // "vite-plugin-svg-icons/client"
], // TS ], // TS
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
// //
"@/*": ["./src/*"], "@/*": [
"#/*": ["types/*"] "./src/*"
],
"#/*": [
"types/*"
]
}, },
"noImplicitAny": false, "noImplicitAny": false,
} }