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

View File

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

View File

@ -23,4 +23,28 @@ export interface PluginConfig {
options: Record<string, any>; options: Record<string, any>;
script: Record<string, any>[]; script: Record<string, any>[];
scriptType: string; 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); await tableStore.initColumn(props.tableKey, props.columns);
} }
} }
initColumns();
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), { const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 32, firstColumnWidth: 32,
@ -768,12 +767,20 @@
handleMustContainColChange(true); handleMustContainColChange(true);
handleTypeCheckingColChange(true); handleTypeCheckingColChange(true);
} }
watch( watch(
() => props.params, () => props.params,
(arr) => { (arr) => {
if (arr.length > 0) { 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) { if (!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault) {
addTableLine(arr.length - 1); addTableLine(arr.length - 1);
} }
@ -934,6 +941,8 @@
defineExpose({ defineExpose({
addTableLine, addTableLine,
}); });
await initColumns();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

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

View File

@ -98,13 +98,15 @@
<a-input <a-input
v-if="props.isDefinition" v-if="props.isDefinition"
v-model:model-value="requestVModel.name" v-model:model-value="requestVModel.name"
class="mt-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')" :placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear allow-clear
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
</div> </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 <MsSplitBox
ref="splitBoxRef" ref="splitBoxRef"
v-model:size="splitBoxSize" v-model:size="splitBoxSize"
@ -207,8 +209,10 @@
:hide-layout-switch="props.hideResponseLayoutSwitch" :hide-layout-switch="props.hideResponseLayoutSwitch"
:request="requestVModel" :request="requestVModel"
:loading="requestVModel.executeLoading" :loading="requestVModel.executeLoading"
:is-edit="props.isDefinition"
@change-expand="changeExpand" @change-expand="changeExpand"
@change-layout="handleActiveLayoutChange" @change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange"
/> />
</template> </template>
</MsSplitBox> </MsSplitBox>
@ -272,7 +276,7 @@
import debugAuth from './auth.vue'; import debugAuth from './auth.vue';
import postcondition from './postcondition.vue'; import postcondition from './postcondition.vue';
import precondition from './precondition.vue'; import precondition from './precondition.vue';
import response from './response.vue'; import response from './response/index.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';
@ -321,7 +325,7 @@
const props = defineProps<{ const props = defineProps<{
request: RequestParam; // request: RequestParam; //
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
detailLoading: boolean; // detailLoading?: boolean; //
isDefinition?: boolean; // isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; // hideResponseLayoutSwitch?: boolean; //
executeApi: (...args) => Promise<any>; // executeApi: (...args) => Promise<any>; //
@ -340,6 +344,7 @@
requestVModel.value.executeLoading = false; // loading requestVModel.value.executeLoading = false; // loading
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP'); const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const temporaryResponseMap = {}; // websockettab const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false);
watch( watch(
() => props.request.id, () => props.request.id,
@ -354,8 +359,8 @@
); );
function handleActiveDebugChange() { function handleActiveDebugChange() {
if (!loading.value) { if (!loading.value && !isHttpProtocol.value && isInitPluginForm.value) {
// change // change
requestVModel.value.unSaved = true; requestVModel.value.unSaved = true;
} }
} }
@ -364,7 +369,7 @@
const commonContentTabKey = [ const commonContentTabKey = [
RequestComposition.PRECONDITION, RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION, RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION, // RequestComposition.ASSERTION, TODO:
]; ];
// tab // tab
const pluginContentTab = [ const pluginContentTab = [
@ -514,12 +519,15 @@
const formData = tempForm || requestVModel.value; const formData = tempForm || requestVModel.value;
if (fApi.value) { if (fApi.value) {
const form = {}; const form = {};
fApi.value.fields().forEach((key) => { pluginScriptMap.value[requestVModel.value.protocol].fields?.forEach((key) => {
form[key] = formData[key]; form[key] = formData[key];
}); });
fApi.value?.setValue(form); fApi.value?.setValue(form);
} }
}); });
nextTick(() => {
isInitPluginForm.value = true;
});
} else { } else {
// form-create tab // form-create tab
nextTick(() => { 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> <template>
<div class="flex h-full min-w-[300px] flex-col"> <div class="flex h-full min-w-[300px] flex-col">
<div <div :class="['response-head', props.isExpanded ? '' : 'border-t']">
class="flex flex-wrap items-center justify-between gap-[8px] border-b border-[var(--color-text-n8)] p-[8px_16px]"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<template v-if="props.activeLayout === 'vertical'"> <template v-if="props.activeLayout === 'vertical'">
<MsButton <MsButton
@ -25,7 +23,27 @@
<icon-right :size="8" /> <icon-right :size="8" />
</MsButton> </MsButton>
</template> </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 <a-radio-group
v-if="!props.hideLayoutSwitch" v-if="!props.hideLayoutSwitch"
v-model:model-value="innerLayout" v-model:model-value="innerLayout"
@ -102,120 +120,99 @@
</div> </div>
</div> </div>
<a-spin :loading="props.loading" class="h-[calc(100%-42px)] w-full px-[18px] pb-[18px]"> <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)]"> <div v-if="props.isEdit" class="my-[8px] w-full">
<a-tab-pane v-for="item of responseTabList" :key="item.value" :title="item.label" /> <MsEditableTab
</a-tabs> v-model:active-tab="activeResponse"
<div class="response-container"> v-model:tabs="responseTabs"
<MsCodeEditor at-least-one
v-if="activeTab === ResponseComposition.BODY" hide-more-action
ref="responseEditorRef" @add="addResponseTab"
: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> <template #label="{ tab }">
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript"> <div class="response-tab">
<template #icon> <div v-if="tab.isDefault" class="response-tab-default-icon"></div>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" /> {{ tab.label }}({{ tab.code }})
</template> <MsMoreAction
</a-button> :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> </template>
</MsCodeEditor> </MsEditableTab>
<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>
</div> </div>
<result
v-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
v-model:activeTab="activeTab"
:response="props.response"
:request="props.request"
/>
</a-spin> </a-spin>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import type { Direction } from '@/components/pure/ms-split-box/index.vue'; import type { Direction } from '@/components/pure/ms-split-box/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import useTable from '@/components/pure/ms-table/useTable'; import result from './result.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue'; import popConfirm from '@/views/api-test/components/popConfirm.vue';
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue'; import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ResponseResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum'; import { ResponseComposition } from '@/enums/apiEnum';
import type { RequestParam } from './index.vue'; 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;
}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
activeTab: keyof typeof ResponseComposition; activeTab: keyof typeof ResponseComposition;
activeLayout?: Direction; activeLayout?: Direction;
isExpanded: boolean; isExpanded: boolean;
response: Response; response: ResponseResult;
request?: RequestParam; request?: RequestParam;
hideLayoutSwitch?: boolean; // hideLayoutSwitch?: boolean; //
loading?: boolean; loading?: boolean;
isEdit?: boolean; //
}>(), }>(),
{ {
activeLayout: 'vertical', activeLayout: 'vertical',
@ -227,6 +224,7 @@
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void; (e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
(e: 'changeExpand', value: boolean): void; (e: 'changeExpand', value: boolean): void;
(e: 'changeLayout', value: Direction): void; (e: 'changeLayout', value: Direction): void;
(e: 'change'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -267,115 +265,102 @@
} }
return 'rgb(var(--danger-7)'; 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(); export interface ResponseItem extends TabItem {
isDefault?: boolean; // tab
function copyScript() { code: number; //
if (isSupported) { showPopConfirm?: boolean; //
copy(props.response.requestResults[0].responseResult.body); showRenamePopConfirm?: boolean; //
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
} }
function getResponsePreContent(type: keyof typeof ResponseComposition) { const activeResponseType = ref<'content' | 'result'>('content');
switch (type) {
case ResponseComposition.HEADER: function setActiveResponse(val: 'content' | 'result') {
return props.response.requestResults[0].responseResult.headers.trim(); activeResponseType.value = val;
case ResponseComposition.REAL_REQUEST: }
return props.response.requestResults[0].body
? `${t('apiTestDebug.requestUrl')}:\n${props.request?.url}\n${t('apiTestDebug.header')}:\n${ const responseTabs = ref<ResponseItem[]>([
props.response.requestResults[0].headers {
}\nBody:\n${props.response.requestResults[0].body.trim()}` id: new Date().getTime(),
: ''; label: t('apiTestManagement.response'),
// case ResponseComposition.EXTRACT: closable: false,
// return Object.keys(props.response.extract) code: 200,
// .map((e) => `${e}: ${props.response.extract[e]}`) isDefault: true,
// .join('\n'); // TODO: 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: default:
return ''; break;
} }
} }
const columns: MsTableColumn = [ function handleDeleteResponseTab(id: number | string) {
{ responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
title: 'apiTestDebug.content', if (id === activeResponse.value.id) {
dataIndex: 'content', [activeResponse.value] = responseTabs.value;
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> </script>
<style lang="less"> <style lang="less">
@ -391,21 +376,37 @@
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
.response-container { .response-head {
margin-top: 8px; @apply flex flex-wrap items-center justify-between border-b;
height: calc(100% - 48px);
.response-header-pre {
@apply h-full overflow-auto bg-white;
.ms-scroll-bar();
padding: 8px 12px; padding: 8px 16px;
border-radius: var(--border-radius-small); 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> </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> </template>
</MsEditableTab> </MsEditableTab>
</div> </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" /> <apiTable :active-module="props.activeModule" :offspring-ids="props.offspringIds" />
</div> </div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden"> <div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
@ -20,12 +20,12 @@
v-model:detail-loading="loading" v-model:detail-loading="loading"
v-model:request="activeApiTab" v-model:request="activeApiTab"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
hide-response-layout-swicth hide-response-layout-switch
:create-api="addDebug" :create-api="addDebug"
:update-api="updateDebug" :update-api="updateDebug"
:execute-api="executeDebug" :execute-api="executeDebug"
:local-execute-api="localExecuteApiDebug" :local-execute-api="localExecuteApiDebug"
is-definiton is-definition
@add-done="emit('addDone')" @add-done="emit('addDone')"
/> />
</template> </template>
@ -48,8 +48,9 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="case" :title="t('apiTestManagement.case')" 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 key="mock" title="MOCK" class="ms-api-tab-pane"> </a-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> <template #extra>
<div class="flex items-center gap-[8px] pr-[24px]"> <div class="flex items-center gap-[8px] pr-[24px]">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]"> <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 initDefaultId = `debug-${Date.now()}`;
const defaultBodyParams: ExecuteBody = { const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE, bodyType: RequestBodyFormat.NONE,
@ -236,6 +247,7 @@
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
isNew: true, isNew: true,
}; };
function addApiTab(defaultProps?: Partial<TabItem>) { function addApiTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`; const id = `debug-${Date.now()}`;
apiTabs.value.push({ apiTabs.value.push({
@ -247,11 +259,6 @@
...defaultProps, ...defaultProps,
}); });
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam; activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
nextTick(() => {
if (defaultProps) {
handleActiveDebugChange();
}
});
} }
const loading = ref(false); const loading = ref(false);

View File

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

View File

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

View File

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