feat(接口管理): 接口定义页面-响应

This commit is contained in:
baiqi 2024-02-28 15:39:47 +08:00 committed by Craftsman
parent f7fcd8b690
commit f02b147f2e
14 changed files with 576 additions and 241 deletions

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="7.5" fill="url(#paint0_linear_9153_382902)" stroke="white"/>
<path d="M8.12 9.272V9.728C7.32 9.808 6.12 9.88 4.52 9.936L4.456 9.448C5.016 9.44 5.544 9.424 6.032 9.408V8.72H4.592V8.256H6.032V7.6H4.552V4.76H8.064V7.6H6.544V8.256H8V8.72H6.544V9.384C7.136 9.352 7.664 9.32 8.12 9.272ZM4.704 10.232L5.184 10.288C5.136 10.872 5.032 11.392 4.856 11.84L4.376 11.688C4.552 11.264 4.656 10.776 4.704 10.232ZM6.008 10.288C6.088 10.624 6.144 11.096 6.176 11.688L5.688 11.744C5.672 11.136 5.624 10.672 5.56 10.336L6.008 10.288ZM7.784 10.128C7.968 10.56 8.112 10.96 8.216 11.328C8.928 10.128 9.336 8.744 9.448 7.184H8.44V6.632H9.472C9.488 5.696 9.496 4.984 9.496 4.488H10.048C10.048 5.08 10.04 5.8 10.032 6.632H11.44V7.184H10.064C10.312 9 10.856 10.4 11.696 11.384L11.304 11.816C10.592 10.912 10.104 9.776 9.848 8.408C9.592 9.752 9.152 10.896 8.528 11.824L8.16 11.416C8.168 11.4 8.176 11.384 8.184 11.376L7.8 11.52C7.688 11.08 7.544 10.656 7.376 10.24L7.784 10.128ZM6.896 10.2C7.04 10.584 7.16 11.016 7.272 11.512L6.808 11.632C6.72 11.184 6.608 10.752 6.48 10.336L6.896 10.2ZM10.72 4.752C11.072 5.2 11.336 5.6 11.52 5.936L11.12 6.224C10.928 5.856 10.664 5.44 10.328 4.992L10.72 4.752ZM7.6 7.152V5.208H6.504V7.152H7.6ZM6.088 7.152V5.208H5.016V7.152H6.088ZM7.008 5.624L7.312 5.696C7.264 6.08 7.176 6.456 7.048 6.824L6.744 6.72C6.872 6.376 6.96 6.008 7.008 5.624ZM5.544 5.656C5.664 5.976 5.768 6.336 5.864 6.744L5.568 6.824C5.48 6.456 5.368 6.096 5.24 5.752L5.544 5.656Z" fill="#783887"/>
<defs>
<linearGradient id="paint0_linear_9153_382902" x1="0" y1="8" x2="16" y2="8" gradientUnits="userSpaceOnUse">
<stop offset="0.0336452" stop-color="#F2E9F6"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -146,7 +146,7 @@
}
};
const defualtMoreActionList = [
const defaultMoreActionList = [
{
eventTag: 'closeAll',
label: t('ms.editableTab.closeAll'),
@ -158,8 +158,8 @@
];
const mergedMoreActionList = computed(() => {
const dl = props.atLeastOne
? defualtMoreActionList.filter((e) => e.eventTag !== 'closeAll')
: defualtMoreActionList;
? defaultMoreActionList.filter((e) => e.eventTag !== 'closeAll')
: defaultMoreActionList;
return props.moreActionList ? [...dl, ...props.moreActionList] : dl;
});

View File

@ -1,11 +1,18 @@
<template>
<span>
<a-dropdown :trigger="props.trigger || 'hover'" @select="selectHandler" @popup-visible-change="visibleChange">
<slot>
<MsButton type="text" size="mini" class="more-icon">
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
</MsButton>
</slot>
<a-dropdown
v-model:popup-visible="visible"
:trigger="props.trigger || 'hover'"
@select="selectHandler"
@popup-visible-change="visibleChange"
>
<div :class="['ms-more-action-trigger-content', visible ? 'ms-more-action-trigger-content--focus' : '']">
<slot>
<MsButton type="text" size="mini" class="more-icon-btn">
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
</MsButton>
</slot>
</div>
<template #content>
<template v-for="item of props.list">
<a-divider
@ -48,6 +55,8 @@
const emit = defineEmits(['select', 'close']);
const visible = ref(false);
// 线action
const beforeDividerHasAction = computed(() => {
let result = false;
@ -99,10 +108,23 @@
color: rgb(var(--danger-6));
}
}
.more-icon {
padding: 4px;
border-radius: var(--border-radius-mini);
&:hover {
.ms-more-action-trigger-content {
@apply flex items-center;
.more-icon-btn {
padding: 2px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9)) !important;
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
}
}
.ms-more-action-trigger-content--focus {
.more-icon-btn {
@apply !visible;
background-color: rgb(var(--primary-9));
.arco-icon {
color: rgb(var(--primary-5));

View File

@ -23,4 +23,28 @@ export interface PluginConfig {
options: Record<string, any>;
script: Record<string, any>[];
scriptType: string;
fields?: string[]; // 插件脚本内配置的全部字段集合
}
// 响应结果
export interface ResponseResult {
requestResults: {
body: string;
headers: string;
responseResult: {
body: string;
contentType: string;
headers: string;
dnsLookupTime: number;
downloadTime: number;
latency: number;
responseCode: number;
responseTime: number;
responseSize: number;
socketInitTime: number;
sslHandshakeTime: number;
tcpHandshakeTime: number;
transferStartTime: number;
};
}[]; // 请求结果
console: string;
}

View File

@ -558,7 +558,6 @@
await tableStore.initColumn(props.tableKey, props.columns);
}
}
initColumns();
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 32,
@ -768,12 +767,20 @@
handleMustContainColChange(true);
handleTypeCheckingColChange(true);
}
watch(
() => props.params,
(arr) => {
if (arr.length > 0) {
propsRes.value.data = arr;
// id
propsRes.value.data = arr.map((item, i) => {
if (!item.id) {
return {
...item,
id: new Date().getTime() + i,
};
}
return item;
});
if (!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault) {
addTableLine(arr.length - 1);
}
@ -934,6 +941,8 @@
defineExpose({
addTableLine,
});
await initColumns();
</script>
<style lang="less" scoped>

View File

@ -63,7 +63,7 @@
}
const props = defineProps<{
mode: 'add' | 'rename';
mode: 'add' | 'rename' | 'tabRename';
nodeType?: 'MODULE' | 'API';
visible?: boolean;
title?: string;
@ -144,6 +144,10 @@
});
Message.success(t('common.updateSuccess'));
emit('renameFinish', form.value.field, props.nodeId);
} else if (props.mode === 'tabRename') {
// tab
Message.success(t('common.updateSuccess'));
emit('renameFinish', form.value.field);
}
if (done) {
done(true);

View File

@ -98,13 +98,15 @@
<a-input
v-if="props.isDefinition"
v-model:model-value="requestVModel.name"
class="mt-[8px]"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear
@change="handleActiveDebugChange"
/>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-40px)]">
<!-- 接口定义多出一个输入框占高度 40px -->
<div ref="splitContainerRef" :class="`${props.isDefinition ? 'h-[calc(100%-80px)]' : 'h-[calc(100%-40px)]'}`">
<MsSplitBox
ref="splitBoxRef"
v-model:size="splitBoxSize"
@ -207,8 +209,10 @@
:hide-layout-switch="props.hideResponseLayoutSwitch"
:request="requestVModel"
:loading="requestVModel.executeLoading"
:is-edit="props.isDefinition"
@change-expand="changeExpand"
@change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange"
/>
</template>
</MsSplitBox>
@ -272,7 +276,7 @@
import debugAuth from './auth.vue';
import postcondition from './postcondition.vue';
import precondition from './precondition.vue';
import response from './response.vue';
import response from './response/index.vue';
import debugSetting from './setting.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
@ -321,7 +325,7 @@
const props = defineProps<{
request: RequestParam; //
moduleTree: ModuleTreeNode[]; //
detailLoading: boolean; //
detailLoading?: boolean; //
isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; //
executeApi: (...args) => Promise<any>; //
@ -340,6 +344,7 @@
requestVModel.value.executeLoading = false; // loading
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false);
watch(
() => props.request.id,
@ -354,8 +359,8 @@
);
function handleActiveDebugChange() {
if (!loading.value) {
// change
if (!loading.value && !isHttpProtocol.value && isInitPluginForm.value) {
// change
requestVModel.value.unSaved = true;
}
}
@ -364,7 +369,7 @@
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
// RequestComposition.ASSERTION, TODO:
];
// tab
const pluginContentTab = [
@ -514,12 +519,15 @@
const formData = tempForm || requestVModel.value;
if (fApi.value) {
const form = {};
fApi.value.fields().forEach((key) => {
pluginScriptMap.value[requestVModel.value.protocol].fields?.forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(form);
}
});
nextTick(() => {
isInitPluginForm.value = true;
});
} else {
// form-create tab
nextTick(() => {

View File

@ -0,0 +1,7 @@
<template>
<div> </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -1,8 +1,6 @@
<template>
<div class="flex h-full min-w-[300px] flex-col">
<div
class="flex flex-wrap items-center justify-between gap-[8px] border-b border-[var(--color-text-n8)] p-[8px_16px]"
>
<div :class="['response-head', props.isExpanded ? '' : 'border-t']">
<div class="flex items-center justify-between">
<template v-if="props.activeLayout === 'vertical'">
<MsButton
@ -25,7 +23,27 @@
<icon-right :size="8" />
</MsButton>
</template>
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
<div
v-if="props.isEdit && props.response.requestResults[0]?.responseResult?.responseCode"
class="ml-[4px] flex items-center"
>
<MsButton
type="text"
:class="['font-medium', activeResponseType === 'content' ? '' : '!text-[var(--color-text-n4)]']"
@click="() => setActiveResponse('content')"
>
{{ t('apiTestDebug.responseContent') }}
</MsButton>
<a-divider direction="vertical" :margin="4"></a-divider>
<MsButton
type="text"
:class="['font-medium', activeResponseType === 'result' ? '' : '!text-[var(--color-text-n4)]']"
@click="() => setActiveResponse('result')"
>
{{ t('apiTestManagement.executeResult') }}
</MsButton>
</div>
<div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
<a-radio-group
v-if="!props.hideLayoutSwitch"
v-model:model-value="innerLayout"
@ -102,120 +120,99 @@
</div>
</div>
<a-spin :loading="props.loading" class="h-[calc(100%-42px)] w-full px-[18px] pb-[18px]">
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
<a-tab-pane v-for="item of responseTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.response.requestResults[0]?.responseResult?.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
<div v-if="props.isEdit" class="my-[8px] w-full">
<MsEditableTab
v-model:active-tab="activeResponse"
v-model:tabs="responseTabs"
at-least-one
hide-more-action
@add="addResponseTab"
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.isDefault" class="response-tab-default-icon"></div>
{{ tab.label }}({{ tab.code }})
<MsMoreAction
:list="
tab.isDefault
? tabMoreActionList.filter((e) => e.eventTag !== 'setDefault' && e.eventTag !== 'delete')
: tabMoreActionList
"
class="response-more-action"
icon-mode="hide"
@select="(e) => handleMoreActionSelect(e, tab as ResponseItem)"
/>
<popConfirm
v-model:visible="tab.showRenamePopConfirm"
mode="tabRename"
:field-config="{ field: tab.label }"
:all-names="responseTabs.map((e) => e.label)"
@rename-finish="(val) => (tab.label = val)"
>
<span :id="`renameSpan${tab.id}`" class="relative"></span>
</popConfirm>
<a-popconfirm
v-model:popup-visible="tab.showPopConfirm"
position="bottom"
content-class="w-[300px]"
:ok-text="t('common.confirm')"
:popup-offset="20"
@ok="() => handleDeleteResponseTab(tab.id)"
>
<template #icon>
<icon-exclamation-circle-fill class="!text-[rgb(var(--danger-6))]" />
</template>
<template #content>
<div class="font-semibold text-[var(--color-text-1)]">
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
</div>
</template>
<div class="relative"></div>
</a-popconfirm>
</div>
</template>
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="response.console.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
<div
v-else-if="
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<pre class="response-header-pre">{{ getResponsePreContent(activeTab) }}</pre>
</div>
<MsBaseTable v-else-if="activeTab === 'ASSERTION'" v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.status === 1 ? 'success' : 'danger'" theme="light">
{{ record.status === 1 ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
</MsEditableTab>
</div>
<result
v-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
v-model:activeTab="activeTab"
:response="props.response"
:request="props.request"
/>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import type { Direction } from '@/components/pure/ms-split-box/index.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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import result from './result.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
import { useI18n } from '@/hooks/useI18n';
import { ResponseResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
import type { RequestParam } from './index.vue';
export interface Response {
requestResults: {
body: string;
headers: string;
responseResult: {
body: string;
contentType: string;
headers: string;
dnsLookupTime: number;
downloadTime: number;
latency: number;
responseCode: number;
responseTime: number;
responseSize: number;
socketInitTime: number;
sslHandshakeTime: number;
tcpHandshakeTime: number;
transferStartTime: number;
};
}[]; //
console: string;
}
import type { RequestParam } from '../index.vue';
const props = withDefaults(
defineProps<{
activeTab: keyof typeof ResponseComposition;
activeLayout?: Direction;
isExpanded: boolean;
response: Response;
response: ResponseResult;
request?: RequestParam;
hideLayoutSwitch?: boolean; //
loading?: boolean;
isEdit?: boolean; //
}>(),
{
activeLayout: 'vertical',
@ -227,6 +224,7 @@
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
(e: 'changeExpand', value: boolean): void;
(e: 'changeLayout', value: Direction): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
@ -267,115 +265,102 @@
}
return 'rgb(var(--danger-7)';
});
//
const responseLanguage = computed(() => {
const { contentType } = props.response.requestResults[0].responseResult;
if (contentType.includes('json')) {
return LanguageEnum.JSON;
}
if (contentType.includes('html')) {
return LanguageEnum.HTML;
}
if (contentType.includes('xml')) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const responseEditorRef = ref<InstanceType<typeof MsCodeEditor>>();
const responseTabList = [
{
label: t('apiTestDebug.responseBody'),
value: ResponseComposition.BODY,
},
{
label: t('apiTestDebug.responseHeader'),
value: ResponseComposition.HEADER,
},
{
label: t('apiTestDebug.realRequest'),
value: ResponseComposition.REAL_REQUEST,
},
{
label: t('apiTestDebug.console'),
value: ResponseComposition.CONSOLE,
},
// {
// label: t('apiTestDebug.extract'),
// value: ResponseComposition.EXTRACT,
// },
// {
// label: t('apiTestDebug.assertion'),
// value: ResponseComposition.ASSERTION,
// }, // TODO:
];
/** 响应内容编辑状态逻辑 */
const { copy, isSupported } = useClipboard();
function copyScript() {
if (isSupported) {
copy(props.response.requestResults[0].responseResult.body);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
export interface ResponseItem extends TabItem {
isDefault?: boolean; // tab
code: number; //
showPopConfirm?: boolean; //
showRenamePopConfirm?: boolean; //
}
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.response.requestResults[0].responseResult.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.response.requestResults[0].body
? `${t('apiTestDebug.requestUrl')}:\n${props.request?.url}\n${t('apiTestDebug.header')}:\n${
props.response.requestResults[0].headers
}\nBody:\n${props.response.requestResults[0].body.trim()}`
: '';
// case ResponseComposition.EXTRACT:
// return Object.keys(props.response.extract)
// .map((e) => `${e}: ${props.response.extract[e]}`)
// .join('\n'); // TODO:
const activeResponseType = ref<'content' | 'result'>('content');
function setActiveResponse(val: 'content' | 'result') {
activeResponseType.value = val;
}
const responseTabs = ref<ResponseItem[]>([
{
id: new Date().getTime(),
label: t('apiTestManagement.response'),
closable: false,
code: 200,
isDefault: true,
showPopConfirm: false,
showRenamePopConfirm: false,
},
]);
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
responseTabs.value.push({
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
code: 200,
...defaultProps,
id: new Date().getTime(),
isDefault: false,
showPopConfirm: false,
showRenamePopConfirm: false,
});
activeResponse.value = responseTabs.value[responseTabs.value.length - 1];
emit('change');
}
const tabMoreActionList: ActionsItem[] = [
{
label: t('apiTestManagement.setDefault'),
eventTag: 'setDefault',
},
{
label: t('common.rename'),
eventTag: 'rename',
},
{
label: t('common.copy'),
eventTag: 'copy',
},
{
isDivider: true,
},
{
label: t('common.delete'),
eventTag: 'delete',
danger: true,
},
];
const renameValue = ref('');
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
switch (e.eventTag) {
case 'setDefault':
responseTabs.value = responseTabs.value.map((tab) => {
tab.isDefault = _tab.id === tab.id;
return tab;
});
break;
case 'rename':
renameValue.value = _tab.label || '';
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
break;
case 'copy':
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
break;
case 'delete':
_tab.showPopConfirm = true;
break;
default:
return '';
break;
}
}
const columns: MsTableColumn = [
{
title: 'apiTestDebug.content',
dataIndex: 'content',
showTooltip: true,
},
{
title: 'apiTestDebug.status',
dataIndex: 'status',
slotName: 'status',
width: 80,
},
{
title: '',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: { x: '100%' },
columns,
});
propsRes.value.data = [
{
id: new Date().getTime(),
content: 'Response Code equals: 200',
status: 1,
desc: '',
},
{
id: new Date().getTime(),
content: '$.users[1].age REGEX: 31',
status: 0,
desc: `Value expected to match regexp '31', but it did not match: '30' match: '30'`,
},
] as any;
function handleDeleteResponseTab(id: number | string) {
responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
if (id === activeResponse.value.id) {
[activeResponse.value] = responseTabs.value;
}
}
</script>
<style lang="less">
@ -391,21 +376,37 @@
</style>
<style lang="less" scoped>
.response-container {
margin-top: 8px;
height: calc(100% - 48px);
.response-header-pre {
@apply h-full overflow-auto bg-white;
.ms-scroll-bar();
.response-head {
@apply flex flex-wrap items-center justify-between border-b;
padding: 8px 12px;
border-radius: var(--border-radius-small);
padding: 8px 16px;
border-color: var(--color-text-n8);
gap: 8px;
}
.response-tab {
@apply flex items-center;
.response-tab-default-icon {
@apply rounded-full;
margin-right: 4px;
width: 16px;
height: 16px;
background: url('@/assets/svg/icons/default.svg') no-repeat;
background-size: contain;
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
}
:deep(.response-more-action) {
margin-left: 4px;
.more-icon-btn {
@apply invisible;
}
}
&:hover {
:deep(.response-more-action) {
.more-icon-btn {
@apply visible;
}
}
}
}
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-tabs-tab) {
@apply leading-none;
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.response.requestResults[0].responseResult?.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="props.response.console.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
<div
v-else-if="
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<pre class="response-header-pre">{{ getResponsePreContent(activeTab) }}</pre>
</div>
<MsBaseTable v-else-if="activeTab === 'ASSERTION'" v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.status === 1 ? 'success' : 'danger'" theme="light">
{{ record.status === 1 ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
</div>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import { ResponseResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{
response: ResponseResult;
request?: RequestParam;
}>();
const { t } = useI18n();
const responseCompositionTabList = [
{
label: t('apiTestDebug.responseBody'),
value: ResponseComposition.BODY,
},
{
label: t('apiTestDebug.responseHeader'),
value: ResponseComposition.HEADER,
},
{
label: t('apiTestDebug.realRequest'),
value: ResponseComposition.REAL_REQUEST,
},
{
label: t('apiTestDebug.console'),
value: ResponseComposition.CONSOLE,
},
// {
// label: t('apiTestDebug.extract'),
// value: ResponseComposition.EXTRACT,
// },
// {
// label: t('apiTestDebug.assertion'),
// value: ResponseComposition.ASSERTION,
// }, // TODO:
];
const activeTab = defineModel<keyof typeof ResponseComposition>('activeTab', {
required: true,
});
//
const responseLanguage = computed(() => {
const { contentType } = props.response.requestResults[0].responseResult;
if (contentType.includes('json')) {
return LanguageEnum.JSON;
}
if (contentType.includes('html')) {
return LanguageEnum.HTML;
}
if (contentType.includes('xml')) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const { copy, isSupported } = useClipboard();
function copyScript() {
if (isSupported) {
copy(props.response.requestResults[0].responseResult.body);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.response.requestResults[0].responseResult?.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.response.requestResults[0].body
? `${t('apiTestDebug.requestUrl')}:\n${props.request?.url}\n${t('apiTestDebug.header')}:\n${
props.response.requestResults[0].headers
}\nBody:\n${props.response.requestResults[0].body.trim()}`
: '';
// case ResponseComposition.EXTRACT:
// return Object.keys(props.response.extract)
// .map((e) => `${e}: ${props.response.extract[e]}`)
// .join('\n'); // TODO:
default:
return '';
}
}
const columns: MsTableColumn = [
{
title: 'apiTestDebug.content',
dataIndex: 'content',
showTooltip: true,
},
{
title: 'apiTestDebug.status',
dataIndex: 'status',
slotName: 'status',
width: 80,
},
{
title: '',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: { x: '100%' },
columns,
});
propsRes.value.data = [
{
id: new Date().getTime(),
content: 'Response Code equals: 200',
status: 1,
desc: '',
},
{
id: new Date().getTime(),
content: '$.users[1].age REGEX: 31',
status: 0,
desc: `Value expected to match regexp '31', but it did not match: '30' match: '30'`,
},
] as any;
</script>
<style lang="less" scoped>
.response-container {
margin-top: 8px;
height: calc(100% - 48px);
.response-header-pre {
@apply h-full overflow-auto bg-white;
.ms-scroll-bar();
padding: 8px 12px;
border-radius: var(--border-radius-small);
}
}
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-tabs-tab) {
@apply leading-none;
}
</style>

View File

@ -8,7 +8,7 @@
</template>
</MsEditableTab>
</div>
<div v-show="activeApiTab?.id === 'all'" class="flex-1">
<div v-show="activeApiTab.id === 'all'" class="flex-1">
<apiTable :active-module="props.activeModule" :offspring-ids="props.offspringIds" />
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
@ -20,12 +20,12 @@
v-model:detail-loading="loading"
v-model:request="activeApiTab"
:module-tree="props.moduleTree"
hide-response-layout-swicth
hide-response-layout-switch
:create-api="addDebug"
:update-api="updateDebug"
:execute-api="executeDebug"
:local-execute-api="localExecuteApiDebug"
is-definiton
is-definition
@add-done="emit('addDone')"
/>
</template>
@ -48,8 +48,9 @@
</template>
</MsSplitBox>
</a-tab-pane>
<a-tab-pane key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
</a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
<template #extra>
<div class="flex items-center gap-[8px] pr-[24px]">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
@ -136,6 +137,16 @@
}
}
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
watch(
() => activeApiTab.value.id,
() => {
if (typeof setActiveApi === 'function') {
setActiveApi(activeApiTab.value);
}
}
);
const initDefaultId = `debug-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
@ -236,6 +247,7 @@
response: cloneDeep(defaultResponse),
isNew: true,
};
function addApiTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
apiTabs.value.push({
@ -247,11 +259,6 @@
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
nextTick(() => {
if (defaultProps) {
handleActiveDebugChange();
}
});
}
const loading = ref(false);

View File

@ -151,6 +151,7 @@
isExpandAll?: boolean; //
activeModule?: string | number; // key
readOnly?: boolean; //
activeNodeId?: string | number; // id
}>(),
{
activeModule: 'all',
@ -248,6 +249,16 @@
function setActiveFolder(id: string) {
selectedKeys.value = [id];
}
watch(
() => props.activeNodeId,
(val) => {
if (val) {
selectedKeys.value = [val];
}
}
);
function setFocusNodeKey(node: MsTreeNodeData) {
focusNodeKey.value = node.id || '';
}

View File

@ -4,6 +4,7 @@
<template #first>
<div class="p-[24px]">
<moduleTree
:active-node-id="activeApi?.id"
@init="(val) => (folderTree = val)"
@new-api="newApi"
@import="importDrawerVisible = true"
@ -50,8 +51,11 @@
</template>
<script lang="ts" setup>
import { provide } from 'vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import { RequestParam } from '../components/requestComposition/index.vue';
import importApi from './components/import.vue';
import management from './components/management/index.vue';
import moduleTree from './components/moduleTree.vue';
@ -63,6 +67,7 @@
const allCount = ref(0);
const importDrawerVisible = ref(false);
const offspringIds = ref<string[]>([]);
const activeApi = ref<RequestParam>();
const managementRef = ref<InstanceType<typeof management>>();
function newApi() {
@ -77,6 +82,12 @@
function handleApiNodeClick(node: ModuleTreeNode) {
managementRef.value?.newTab(node);
}
function setActiveApi(params: RequestParam) {
activeApi.value = params;
}
provide('setActiveApi', setActiveApi);
</script>
<style lang="less" scoped></style>

View File

@ -82,4 +82,8 @@ export default {
'apiTestManagement.addPostDependency': '添加后置依赖',
'apiTestManagement.saveAsCase': '保存为新用例',
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
'apiTestManagement.executeResult': '执行结果',
'apiTestManagement.setDefault': '设为默认',
'apiTestManagement.confirmDelete': '确认删除 {name} 吗?',
'apiTestManagement.response': '响应{count}',
};