feat(接口管理): 接口定义页面-响应
This commit is contained in:
parent
f7fcd8b690
commit
f02b147f2e
|
@ -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 |
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||||
|
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(() => {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div> </div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||||
|
|
|
@ -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 || '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue