feat(接口测试):接口管理页面-50%

This commit is contained in:
BAIQI 2024-02-15 15:46:43 +08:00 committed by Craftsman
parent eb56b30e9d
commit 8f7f3b2b08
33 changed files with 1779 additions and 1182 deletions

View File

@ -627,6 +627,9 @@
} }
/** 开关 **/ /** 开关 **/
.arco-switch {
margin-left: 2px; // 避免开关圆形左边被遮挡
}
.arco-switch-type-line.arco-switch-small { .arco-switch-type-line.arco-switch-small {
height: 14px; height: 14px;
line-height: 14px; line-height: 14px;

View File

@ -43,27 +43,29 @@
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { Language } from '@/components/pure/ms-code-editor/types';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestConditionScriptLanguage } from '@/enums/apiEnum'; import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
import type { CommonScriptMenu } from './types'; import type { CommonScriptMenu } from './types';
import { getCodeTemplate, type Languages, SCRIPT_MENU } from './utils'; import { getCodeTemplate, SCRIPT_MENU } from './utils';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
expand: boolean; expand: boolean;
languagesType: Languages | RequestConditionScriptLanguage; languagesType: Language | RequestConditionScriptLanguage;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:expand', value: boolean): void; (e: 'update:expand', value: boolean): void;
(e: 'update:languagesType', value: Languages): void; (e: 'update:languagesType', value: Language): void;
(e: 'insert', code: string): void; (e: 'insert', code: string): void;
(e: 'formApiImport'): void; // api (e: 'formApiImport'): void; // api
(e: 'insertCommonScript'): void; // api (e: 'insertCommonScript'): void; // api
(e: 'updateLanguages', value: Languages): void; // api (e: 'updateLanguages', value: Language): void; // api
}>(); }>();
const innerExpand = useVModel(props, 'expand', emit); const innerExpand = useVModel(props, 'expand', emit);
@ -144,7 +146,7 @@
function changeHandler( function changeHandler(
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[] value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) { ) {
innerLanguageType.value = value as Languages; innerLanguageType.value = value as Language;
} }
</script> </script>

View File

@ -88,14 +88,12 @@
import type { CommonScriptItem } from '@/models/projectManagement/commonScript'; import type { CommonScriptItem } from '@/models/projectManagement/commonScript';
import { RequestConditionScriptLanguage } from '@/enums/apiEnum'; import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
import { type Languages } from './utils';
const appStore = useAppStore(); const appStore = useAppStore();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
showType: 'commonScript' | 'executionResult'; // showType: 'commonScript' | 'executionResult'; //
language: Languages | RequestConditionScriptLanguage; language: Language | RequestConditionScriptLanguage;
code: string; code: string;
enableRadioSelected?: boolean; enableRadioSelected?: boolean;
executionResult?: string; // executionResult?: string; //
@ -106,7 +104,7 @@
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:language', value: Languages | RequestConditionScriptLanguage): void; (e: 'update:language', value: Language | RequestConditionScriptLanguage): void;
(e: 'update:code', value: string): void; (e: 'update:code', value: string): void;
}>(); }>();

View File

@ -157,22 +157,23 @@
async function testApi() { async function testApi() {
try { try {
testApiLoading.value = true; testApiLoading.value = true;
if (apiConfig.value.id) {
//
await updateLocalConfig({
id: apiConfig.value.id,
userUrl: apiConfig.value.userUrl.trim(),
});
} else {
const result = await addLocalConfig({
type: 'API',
userUrl: apiConfig.value.userUrl.trim(),
});
apiConfig.value.id = result.id;
}
const res = await validLocalConfig(apiConfig.value.id); const res = await validLocalConfig(apiConfig.value.id);
apiConfig.value.status = res ? 1 : 2; apiConfig.value.status = res ? 1 : 2;
if (res) { if (res) {
//
if (apiConfig.value.id) {
//
await updateLocalConfig({
id: apiConfig.value.id,
userUrl: apiConfig.value.userUrl.trim(),
});
} else {
const result = await addLocalConfig({
type: 'API',
userUrl: apiConfig.value.userUrl.trim(),
});
apiConfig.value.id = result.id;
}
Message.success(t('ms.personal.testPass')); Message.success(t('ms.personal.testPass'));
} else { } else {
Message.error(t('ms.personal.testFail')); Message.error(t('ms.personal.testFail'));

View File

@ -35,7 +35,11 @@
<slot name="extra" v-bind="_props"></slot> <slot name="extra" v-bind="_props"></slot>
<MsTableMoreAction <MsTableMoreAction
v-if="props.nodeMoreActions" v-if="props.nodeMoreActions"
:list="props.nodeMoreActions" :list="
typeof props.filterMoreActionFunc === 'function'
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
: props.nodeMoreActions
"
trigger="click" trigger="click"
@select="handleNodeMoreSelect($event, _props)" @select="handleNodeMoreSelect($event, _props)"
@close="moreActionsClose" @close="moreActionsClose"
@ -112,6 +116,7 @@
| 'right' | 'right'
| 'rt' | 'rt'
| 'rb'; // tooltip | 'rb'; // tooltip
filterMoreActionFunc?: (items: ActionsItem[], node: MsTreeNodeData) => ActionsItem[]; //
}>(), }>(),
{ {
searchDebounce: 300, searchDebounce: 300,

View File

@ -16,7 +16,7 @@
v-for="tab in props.tabs" v-for="tab in props.tabs"
:key="tab.id" :key="tab.id"
class="ms-editable-tab" class="ms-editable-tab"
:class="{ active: innerActiveTab === tab.id }" :class="{ active: innerActiveTab?.id === tab.id }"
@click="handleTabClick(tab)" @click="handleTabClick(tab)"
> >
<div :draggable="!!tab.draggable" class="flex items-center"> <div :draggable="!!tab.draggable" class="flex items-center">
@ -46,6 +46,7 @@
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<a-tooltip <a-tooltip
v-if="!props.readonly"
:content="t('ms.editableTab.limitTip', { max: props.limit })" :content="t('ms.editableTab.limitTip', { max: props.limit })"
:disabled="!props.limit || props.tabs.length >= props.limit" :disabled="!props.limit || props.tabs.length >= props.limit"
> >
@ -60,9 +61,9 @@
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<MsMoreAction <MsMoreAction
v-if="props.moreActionList" v-if="!props.hideMoreAction && !props.readonly"
:list="props.moreActionList" :list="mergedMoreActionList"
@select="(val) => emit('moreActionSelect', val)" @select="handleMoreActionSelect"
> >
<MsButton type="icon" status="secondary" class="ms-editable-tab-button"> <MsButton type="icon" status="secondary" class="ms-editable-tab-button">
<MsIcon type="icon-icon_more_outlined" /> <MsIcon type="icon-icon_more_outlined" />
@ -82,19 +83,22 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import type { TabItem } from './types'; import type { TabItem } from './types';
const props = defineProps<{ const props = defineProps<{
tabs: TabItem[]; tabs: TabItem[];
activeTab: string | number; activeTab?: TabItem;
moreActionList?: ActionsItem[]; moreActionList?: ActionsItem[];
limit?: number; // tab limit?: number; // tab
atLeastOne?: boolean; // tab atLeastOne?: boolean; // tab
hideMoreAction?: boolean; //
readonly?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:tabs', activeTab: string | number): void; (e: 'update:tabs', tabs: TabItem[]): void;
(e: 'update:activeTab', activeTab: string | number): void; (e: 'update:activeTab', activeTab: TabItem): void;
(e: 'add'): void; (e: 'add'): void;
(e: 'close', item: TabItem): void; (e: 'close', item: TabItem): void;
(e: 'change', item: TabItem): void; (e: 'change', item: TabItem): void;
@ -102,10 +106,11 @@
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal();
const innerActiveTab = useVModel(props, 'activeTab', emit); const innerActiveTab = useVModel(props, 'activeTab', emit);
const innerTabs = useVModel(props, 'tabs', emit); const innerTabs = useVModel(props, 'tabs', emit);
const tabNav = ref<HTMLElement | null>(null); const tabNav = ref<HTMLElement>();
const { arrivedState } = useScroll(tabNav); const { arrivedState } = useScroll(tabNav);
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); //
@ -129,7 +134,7 @@
}; };
const scrollToActiveTab = () => { const scrollToActiveTab = () => {
const activeTabDom = tabNav.value?.querySelector('.tab.active'); const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active');
if (activeTabDom) { if (activeTabDom) {
const tabRect = activeTabDom.getBoundingClientRect(); const tabRect = activeTabDom.getBoundingClientRect();
const navRect = tabNav.value?.getBoundingClientRect(); const navRect = tabNav.value?.getBoundingClientRect();
@ -141,22 +146,35 @@
} }
}; };
const defualtMoreActionList = [
{
eventTag: 'closeAll',
label: t('ms.editableTab.closeAll'),
},
{
eventTag: 'closeOther',
label: t('ms.editableTab.closeOther'),
},
];
const mergedMoreActionList = computed(() => {
const dl = props.atLeastOne
? defualtMoreActionList.filter((e) => e.eventTag !== 'closeAll')
: defualtMoreActionList;
return props.moreActionList ? [...dl, ...props.moreActionList] : dl;
});
watch( watch(
() => props.activeTab, () => props.activeTab,
(val) => { () => {
emit('change', props.tabs.find((item) => item.id === val) as TabItem); useDraggable('.ms-editable-tab-nav', innerTabs, {
ghostClass: 'ms-editable-tab-ghost',
});
nextTick(() => {
scrollToActiveTab();
});
} }
); );
watch(props.tabs, () => {
useDraggable('.ms-editable-tab-nav', innerTabs, {
ghostClass: 'ms-editable-tab-ghost',
});
nextTick(() => {
scrollToActiveTab();
});
});
onMounted(() => { onMounted(() => {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
scrollToActiveTab(); scrollToActiveTab();
@ -168,16 +186,75 @@
emit('add'); emit('add');
} }
function closeOneTab(item: TabItem) {
const index = innerTabs.value.findIndex((e) => e.id === item.id);
innerTabs.value.splice(index, 1);
if (innerActiveTab.value?.id === item.id && innerTabs.value[0]) {
[innerActiveTab.value] = innerTabs.value;
}
}
function close(item: TabItem) { function close(item: TabItem) {
emit('close', item); if (item.unSaved) {
openModal({
title: t('common.tip'),
content: t('ms.editableTab.closeTabTip'),
type: 'warning',
hideCancel: false,
onBeforeOk: async () => {
closeOneTab(item);
emit('close', item);
},
});
} else {
closeOneTab(item);
emit('close', item);
}
} }
function handleTabClick(item: TabItem) { function handleTabClick(item: TabItem) {
emit('change', item); innerActiveTab.value = item;
innerActiveTab.value = item.id;
nextTick(() => { nextTick(() => {
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}); });
emit('change', item);
}
function executeAction(event: ActionsItem) {
switch (event.eventTag) {
case 'closeAll':
innerTabs.value = innerTabs.value.filter((item) => item.closable === false);
[innerActiveTab.value] = innerTabs.value;
break;
case 'closeOther':
innerTabs.value = innerTabs.value.filter(
(item) => item.id === innerActiveTab.value?.id || item.closable === false
);
break;
default:
emit('moreActionSelect', event);
break;
}
}
function handleMoreActionSelect(event: ActionsItem) {
if (
(event.eventTag === 'closeAll' && innerTabs.value.some((item) => item.unSaved)) ||
(event.eventTag === 'closeOther' &&
innerTabs.value.some((item) => item.unSaved && item.id !== innerActiveTab.value?.id))
) {
openModal({
title: t('common.tip'),
content: t('ms.editableTab.batchCloseTabTip'),
type: 'warning',
hideCancel: false,
onBeforeOk: async () => {
executeAction(event);
},
});
return;
}
executeAction(event);
} }
</script> </script>

View File

@ -2,4 +2,10 @@ export default {
'ms.editableTab.arrivedLeft': 'Already reached the far left~', 'ms.editableTab.arrivedLeft': 'Already reached the far left~',
'ms.editableTab.arrivedRight': 'Already reached the far right~', 'ms.editableTab.arrivedRight': 'Already reached the far right~',
'ms.editableTab.limitTip': 'Up to {max} tabs can currently be open', 'ms.editableTab.limitTip': 'Up to {max} tabs can currently be open',
'ms.editableTab.closeTabTip':
'The modified content of this tab has not been saved. The unsaved content will be lost after closing. Are you sure you want to close?',
'ms.editableTab.batchCloseTabTip':
'The content of some tabs has not been saved. The unsaved content will be lost after closing. Are you sure you want to close?',
'ms.editableTab.closeAll': 'Close all',
'ms.editableTab.closeOther': 'Close other',
}; };

View File

@ -2,4 +2,8 @@ export default {
'ms.editableTab.arrivedLeft': '到最左侧啦~', 'ms.editableTab.arrivedLeft': '到最左侧啦~',
'ms.editableTab.arrivedRight': '到最右侧啦~', 'ms.editableTab.arrivedRight': '到最右侧啦~',
'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页', 'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页',
'ms.editableTab.closeTabTip': '该标签页有改动的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
'ms.editableTab.batchCloseTabTip': '有标签页的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
'ms.editableTab.closeAll': '关闭全部',
'ms.editableTab.closeOther': '关闭其他',
}; };

View File

@ -1,4 +1,4 @@
import { type Languages } from '@/components/business/ms-common-script/utils'; import { Language } from '@/components/pure/ms-code-editor/types';
export interface CommonScriptMenu { export interface CommonScriptMenu {
title: string; title: string;
@ -13,7 +13,7 @@ export interface CommonScriptItem {
name: string; name: string;
tags: string[]; tags: string[];
description: string; description: string;
type: Languages; // 脚本语言类型 type: Language; // 脚本语言类型
status: string; // 脚本状态(进行中/已完成) status: string; // 脚本状态(进行中/已完成)
createTime: number; createTime: number;
updateTime: number; updateTime: number;
@ -29,7 +29,7 @@ export interface AddOrUpdateCommonScript {
id?: string; id?: string;
projectId: string; projectId: string;
name: string; name: string;
type: Languages; type: Language;
status: string; status: string;
tags: string[]; tags: string[];
description: string; description: string;

View File

@ -226,16 +226,16 @@ export function filterTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[], tree: TreeNode<T> | TreeNode<T>[] | T | T[],
filterFn: (node: TreeNode<T>) => boolean, filterFn: (node: TreeNode<T>) => boolean,
customChildrenKey = 'children' customChildrenKey = 'children'
): TreeNode<T>[] { ): T[] {
if (!Array.isArray(tree)) { if (!Array.isArray(tree)) {
tree = [tree]; tree = [tree];
} }
const filteredTree: TreeNode<T>[] = []; const filteredTree: T[] = [];
for (let i = 0; i < tree.length; i++) { for (let i = 0; i < tree.length; i++) {
const node = tree[i]; const node = tree[i];
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点 // 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
if (filterFn(node)) { if (filterFn(node)) {
const newNode: TreeNode<T> = { ...node }; const newNode: T = { ...node };
if (node[customChildrenKey] && node[customChildrenKey].length > 0) { if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中 // 递归过滤子节点,并将过滤后的子节点添加到当前节点中
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey); newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);

View File

@ -8,7 +8,7 @@
</a-radio-group> </a-radio-group>
<div <div
v-if="!condition.enableCommonScript" v-if="!condition.enableCommonScript"
class="relative rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]" class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
> >
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]"> <div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<a-input <a-input
@ -84,13 +84,15 @@
</a-button> </a-button>
</div> </div>
</div> </div>
<MsScriptDefined <div class="h-[calc(100%-24px)] min-h-[300px]">
v-if="condition.script !== undefined && condition.scriptLanguage !== undefined" <MsScriptDefined
v-model:code="condition.script" v-if="condition.script !== undefined && condition.scriptLanguage !== undefined"
v-model:language="condition.scriptLanguage" v-model:code="condition.script"
show-type="commonScript" v-model:language="condition.scriptLanguage"
:show-header="false" show-type="commonScript"
></MsScriptDefined> :show-header="false"
/>
</div>
</div> </div>
<div v-else class="flex h-[calc(100%-47px)] flex-col"> <div v-else class="flex h-[calc(100%-47px)] flex-col">
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]"> <div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
@ -443,11 +445,19 @@ org.apache.http.client.method . . . '' at line number 2
columns, columns,
noDisable: true, noDisable: true,
}); });
watch(
() => condition.value.params,
(arr) => {
propsRes.value.data = arr as any[]; //
}
);
const showQuoteDrawer = ref(false); const showQuoteDrawer = ref(false);
function saveQuoteScriptHandler(item: any) { function saveQuoteScriptHandler(item: any) {
condition.value.script = item.script; condition.value.script = item.script;
condition.value.scriptId = item.id; condition.value.scriptId = item.id;
condition.value.scriptName = item.name; condition.value.scriptName = item.name; // TODO:
condition.value.params = (JSON.parse(item.params) || []).map((e: any) => { condition.value.params = (JSON.parse(item.params) || []).map((e: any) => {
return { return {
key: e.name, key: e.name,
@ -690,7 +700,7 @@ org.apache.http.client.method . . . '' at line number 2
background-color: var(--color-text-n9); background-color: var(--color-text-n9);
} }
.condition-content { .condition-content {
@apply flex-1 overflow-y-auto; @apply flex flex-1 flex-col overflow-y-auto;
.ms-scroll-bar(); .ms-scroll-bar();
padding: 16px; padding: 16px;

View File

@ -0,0 +1,747 @@
<template>
<div class="flex h-full flex-col">
<div class="px-[24px] pt-[16px]">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex flex-1">
<a-select
v-model:model-value="requsetVModel.protocol"
:options="protocolOptions"
:loading="protocolLoading"
class="mr-[4px] w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="requsetVModel.method"
class="w-[140px]"
@change="handleActiveDebugChange"
/>
<a-input
v-model:model-value="requsetVModel.url"
:max-length="255"
:placeholder="t('apiTestDebug.urlPlaceholder')"
@change="handleActiveDebugChange"
/>
</a-input-group>
</div>
<div class="ml-[16px]">
<a-dropdown-button
:button-props="{ loading: requsetVModel.executeLoading }"
:disabled="requsetVModel.executeLoading"
class="exec-btn"
@click="execute"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon>
<icon-down />
</template>
<template v-if="hasLocalExec" #content>
<a-doption :value="isPriorityLocalExec ? 'localExec' : 'serverExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-dropdown v-if="props.isDefiniton" @select="handleSelect">
<a-button type="secondary">{{ t('common.save') }}</a-button>
<template #content>
<a-doption value="save">{{ t('common.save') }}</a-doption>
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
</template>
</a-dropdown>
<a-button v-else type="secondary" @click="handleSaveShortcut">
<div class="flex items-center">
{{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div>
</div>
</a-button>
</div>
</div>
<a-input
v-if="props.isDefiniton"
v-model:model-value="requsetVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
@change="handleActiveDebugChange"
/>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-52px)]">
<MsSplitBox
ref="splitBoxRef"
v-model:size="splitBoxSize"
:max="0.98"
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
@expand-change="handleExpandChange"
>
<template #first>
<div
:class="`flex h-full min-w-[800px] flex-col px-[24px] pb-[16px] ${
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
}`"
>
<div>
<a-tabs v-model:active-key="requsetVModel.activeTab" class="no-content mb-[16px]">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
</div>
<div class="tab-pane-container">
<template v-if="isInitPluginForm || requsetVModel.activeTab === RequestComposition.PLUGIN">
<a-spin v-show="requsetVModel.activeTab === RequestComposition.PLUGIN" :loading="pluginLoading">
<MsFormCreate v-model:api="fApi" :rule="currentPluginScript" :option="options" />
</a-spin>
</template>
<debugHeader
v-if="requsetVModel.activeTab === RequestComposition.HEADER"
v-model:params="requsetVModel.headers"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugBody
v-else-if="requsetVModel.activeTab === RequestComposition.BODY"
v-model:params="requsetVModel.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugQuery
v-else-if="requsetVModel.activeTab === RequestComposition.QUERY"
v-model:params="requsetVModel.query"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugRest
v-else-if="requsetVModel.activeTab === RequestComposition.REST"
v-model:params="requsetVModel.rest"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="requsetVModel.activeTab === RequestComposition.PRECONDITION"
v-model:config="requsetVModel.children[0].preProcessorConfig"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="requsetVModel.activeTab === RequestComposition.POST_CONDITION"
v-model:config="requsetVModel.children[0].postProcessorConfig"
:response="requsetVModel.response.requestResults[0]?.responseResult.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="requsetVModel.activeTab === RequestComposition.AUTH"
v-model:params="requsetVModel.authConfig"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="requsetVModel.activeTab === RequestComposition.SETTING"
v-model:params="requsetVModel.otherConfig"
@change="handleActiveDebugChange"
/>
</div>
</div>
</template>
<template #second>
<response
v-model:active-layout="activeLayout"
v-model:active-tab="requsetVModel.responseActiveTab"
:is-expanded="isExpanded"
:response="requsetVModel.response"
:hide-layout-swicth="props.hideResponseLayoutSwicth"
@change-expand="changeExpand"
@change-layout="handleActiveLayoutChange"
/>
</template>
</MsSplitBox>
</div>
</div>
<a-modal
v-model:visible="saveModalVisible"
:title="t('common.save')"
:ok-loading="saveLoading"
class="ms-modal-form"
title-align="start"
body-class="!p-0"
@before-ok="handleSave"
@cancel="handleCancel"
>
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
<a-form-item
field="name"
:label="t('apiTestDebug.requestName')"
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
</a-form-item>
<a-form-item
v-if="isHttpProtocol"
field="path"
:label="t('apiTestDebug.requestUrl')"
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.path" :placeholder="t('apiTestDebug.commonPlaceholder')" />
</a-form-item>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select
v-model:modelValue="saveModalForm.moduleId"
:data="selectTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
allow-search
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import debugAuth from './auth.vue';
import postcondition from './postcondition.vue';
import precondition from './precondition.vue';
import response from './response.vue';
import debugSetting from './setting.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { filterTree, getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import { RequestComposition } from '@/enums/apiEnum';
// Http
const debugHeader = defineAsyncComponent(() => import('./header.vue'));
const debugBody = defineAsyncComponent(() => import('./body.vue'));
const debugQuery = defineAsyncComponent(() => import('./query.vue'));
const debugRest = defineAsyncComponent(() => import('./rest.vue'));
export type RequestParam = ExecuteHTTPRequestFullParams & TabItem & Record<string, any>;
const props = defineProps<{
request: RequestParam; //
moduleTree: ModuleTreeNode[]; //
detailLoading: boolean; //
isDefiniton?: boolean; //
hideResponseLayoutSwicth?: boolean; //
executeApi: (...args) => Promise<any>; //
createApi: (...args) => Promise<any>; //
updateApi: (...args) => Promise<any>; //
}>();
const emit = defineEmits(['addDone']);
const appStore = useAppStore();
const { t } = useI18n();
const loading = defineModel('detailLoading', { default: false });
const requsetVModel = defineModel<RequestParam>('request', { required: true });
requsetVModel.value.executeLoading = false; // loading
const isHttpProtocol = computed(() => requsetVModel.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); //
const temporyResponseMap = {}; // websockettab
watch(
() => requsetVModel.value.protocol,
(val) => {
if (val !== 'HTTP') {
isInitPluginForm.value = true;
}
},
{
immediate: true,
}
);
watch(
() => props.request.id,
() => {
if (temporyResponseMap[props.request.reportId]) {
//
requsetVModel.value.response = temporyResponseMap[props.request.reportId];
requsetVModel.value.executeLoading = false;
delete temporyResponseMap[props.request.reportId];
}
}
);
function handleActiveDebugChange() {
if (!loading.value) {
// change
requsetVModel.value.unSaved = true;
}
}
// tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.PRECONDITION,
label: t('apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('apiTestDebug.post'),
},
{
value: RequestComposition.ASSERTION,
label: t('apiTestDebug.assertion'),
},
{
value: RequestComposition.AUTH,
label: t('apiTestDebug.auth'),
},
{
value: RequestComposition.SETTING,
label: t('apiTestDebug.setting'),
},
];
// tab
const contentTabList = computed(() =>
isHttpProtocol.value
? httpContentTabList
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]
);
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const hasLocalExec = ref(false); // api
const isPriorityLocalExec = ref(false); //
async function initLocalConfig() {
try {
const res = await getLocalConfig();
const apiLocalExec = res.find((e) => e.type === 'API');
if (apiLocalExec) {
hasLocalExec.value = true;
isPriorityLocalExec.value = apiLocalExec.enable || false;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const pluginScriptMap = ref<Record<string, any>>({}); //
const pluginLoading = ref(false);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requsetVModel.value.protocol] || []
);
async function initPluginScript() {
if (pluginScriptMap.value[requsetVModel.value.protocol] !== undefined) {
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(
protocolOptions.value.find((e) => e.value === requsetVModel.value.protocol)?.pluginId || ''
);
pluginScriptMap.value[requsetVModel.value.protocol] = res.script;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
function handleActiveDebugProtocolChange(val: string) {
if (val !== 'HTTP') {
requsetVModel.value.activeTab = RequestComposition.PLUGIN;
initPluginScript();
} else {
requsetVModel.value.activeTab = RequestComposition.HEADER;
}
handleActiveDebugChange();
}
const fApi = ref();
const options = {
form: {
labelAlign: 'right',
autoLabelWidth: true,
size: 'small',
hideRequiredAsterisk: false,
showMessage: true,
inlineMessage: false,
scrollToFirstError: true,
},
submitBtn: false,
resetBtn: false,
};
const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0);
watch(
() => splitBoxSize.value,
debounce((val) => {
// 300ms
if (splitContainerRef.value) {
if (typeof val === 'string' && val.includes('px')) {
val = Number(val.split('px')[0]);
secondBoxHeight.value = splitContainerRef.value.clientHeight - val;
} else {
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
}
}
}, 300),
{
immediate: true,
}
);
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isExpanded = ref(true);
function handleExpandChange(val: boolean) {
isExpanded.value = val;
}
function changeExpand(val: boolean) {
isExpanded.value = val;
if (val) {
splitBoxRef.value?.expand(0.6);
} else {
splitBoxRef.value?.collapse(
splitContainerRef.value
? `${splitContainerRef.value.clientHeight - (props.hideResponseLayoutSwicth ? 37 : 42)}px`
: 0
);
}
}
function handleActiveLayoutChange() {
isExpanded.value = true;
splitBoxSize.value = 0.6;
splitBoxRef.value?.expand(0.6);
}
const reportId = ref('');
const websocket = ref<WebSocket>();
function debugSocket() {
websocket.value = getSocket(reportId.value);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (requsetVModel.value.reportId === data.reportId) {
// tabtab
requsetVModel.value.response = data.taskResult;
requsetVModel.value.executeLoading = false;
} else {
// tab
temporyResponseMap[data.reportId] = data.taskResult;
}
}
});
}
function makeRequestParams() {
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requsetVModel.value.protocol
)?.polymorphicName; //
let requestParams;
if (isHttpProtocol.value) {
requestParams = {
authConfig: requsetVModel.value.authConfig,
body: {
...requsetVModel.value.body,
binaryBody: undefined,
formDataBody: {
formValues: requsetVModel.value.body.formDataBody.formValues.filter(
(e, i) => i !== requsetVModel.value.body.formDataBody.formValues.length - 1
), //
},
wwwFormBody: {
formValues: requsetVModel.value.body.wwwFormBody.formValues.filter(
(e, i) => i !== requsetVModel.value.body.wwwFormBody.formValues.length - 1
), //
},
}, // TODO:binaryBody
headers: requsetVModel.value.headers.filter((e, i) => i !== requsetVModel.value.headers.length - 1), //
method: requsetVModel.value.method,
otherConfig: requsetVModel.value.otherConfig,
path: requsetVModel.value.url,
query: requsetVModel.value.query.filter((e, i) => i !== requsetVModel.value.query.length - 1), //
rest: requsetVModel.value.rest.filter((e, i) => i !== requsetVModel.value.rest.length - 1), //
url: requsetVModel.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value.form,
polymorphicName,
};
}
reportId.value = getGenerateId();
requsetVModel.value.reportId = reportId.value; // ID
debugSocket(); // websocket
return {
id: requsetVModel.value.id.toString(),
reportId: reportId.value,
environmentId: '',
tempFileIds: [],
request: {
...requestParams,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
// TODO:
enableGlobal: false,
assertions: [],
},
postProcessorConfig: requsetVModel.value.children[0].postProcessorConfig,
preProcessorConfig: requsetVModel.value.children[0].preProcessorConfig,
},
],
},
projectId: appStore.currentProjectId,
};
}
/**
* 执行调试
* @param val 执行类型
*/
async function execute(execuetType?: 'localExec' | 'serverExec') {
// TODO:&
if (isHttpProtocol.value) {
try {
requsetVModel.value.executeLoading = true;
await props.executeApi(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
requsetVModel.value.executeLoading = false;
}
} else {
//
fApi.value?.validate(async (valid) => {
if (valid === true) {
try {
requsetVModel.value.executeLoading = true;
await props.executeApi(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
requsetVModel.value.executeLoading = false;
}
} else {
requsetVModel.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
});
}
}
const saveModalVisible = ref(false);
const saveModalForm = ref({
name: '',
path: requsetVModel.value.url || '',
moduleId: 'root',
});
const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false);
const selectTree = computed(() =>
filterTree(cloneDeep(props.moduleTree), (e) => {
e.draggable = false;
return e.type === 'MODULE';
})
);
watch(
() => saveModalVisible.value,
(val) => {
if (!val) {
saveModalFormRef.value?.resetFields();
}
}
);
async function handleSaveShortcut() {
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveModalForm.value = {
name: requsetVModel.value.name || '',
path: requsetVModel.value.url || '',
moduleId: 'root',
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
requsetVModel.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
}
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'save':
console.log('save');
break;
case 'saveAsCase':
console.log('saveAsCase');
break;
default:
break;
}
}
function handleCancel() {
saveModalFormRef.value?.resetFields();
}
async function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
saveLoading.value = true;
if (requsetVModel.value.isNew) {
//
await props.createApi({
...makeRequestParams(),
...saveModalForm.value,
protocol: requsetVModel.value.protocol,
method: isHttpProtocol.value ? requsetVModel.value.method : requsetVModel.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
} else {
await props.updateApi({
...makeRequestParams(),
...saveModalForm.value,
protocol: requsetVModel.value.protocol,
method: isHttpProtocol.value ? requsetVModel.value.method : requsetVModel.value.protocol,
uploadFileIds: [],
linkFileIds: [],
deleteFileIds: [], // TODO:
unLinkRefIds: [], // TODO:
});
}
saveLoading.value = false;
saveModalVisible.value = false;
done(true);
requsetVModel.value.unSaved = false;
requsetVModel.value.name = saveModalForm.value.name;
requsetVModel.value.label = saveModalForm.value.name;
emit('addDone');
Message.success(requsetVModel.value.isNew ? t('common.saveSuccess') : t('common.updateSuccess'));
} catch (error) {
saveLoading.value = false;
}
}
});
done(false);
}
onBeforeMount(() => {
initProtocolList();
initLocalConfig();
});
onMounted(() => {
if (!props.isDefiniton) {
registerCatchSaveShortcut(handleSaveShortcut);
}
});
onBeforeUnmount(() => {
if (!props.isDefiniton) {
removeCatchSaveShortcut(handleSaveShortcut);
}
});
</script>
<style lang="less" scoped>
.exec-btn {
margin-right: 12px;
:deep(.arco-btn) {
color: white !important;
background-color: rgb(var(--primary-5)) !important;
.btn-base-primary-hover();
.btn-base-primary-active();
.btn-base-primary-disabled();
}
}
.tab-pane-container {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
}
:deep(.no-content) {
.arco-tabs-content {
display: none;
}
}
</style>

View File

@ -23,6 +23,7 @@
</template> </template>
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div> <div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
<a-radio-group <a-radio-group
v-if="!props.hideLayoutSwicth"
v-model:model-value="innerLayout" v-model:model-value="innerLayout"
type="button" type="button"
size="small" size="small"
@ -183,12 +184,19 @@
console: string; console: string;
} }
const props = defineProps<{ const props = withDefaults(
activeTab: keyof typeof ResponseComposition; defineProps<{
activeLayout: Direction; activeTab: keyof typeof ResponseComposition;
isExpanded: boolean; activeLayout?: Direction;
response: Response; isExpanded: boolean;
}>(); response: Response;
hideLayoutSwicth?: boolean; //
}>(),
{
activeLayout: 'vertical',
hideLayoutSwicth: false,
}
);
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:activeLayout', value: Direction): void; (e: 'update:activeLayout', value: Direction): void;
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void; (e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
@ -285,7 +293,7 @@
// { // {
// label: t('apiTestDebug.assertion'), // label: t('apiTestDebug.assertion'),
// value: ResponseComposition.ASSERTION, // value: ResponseComposition.ASSERTION,
// }, // }, // TODO:
]; ];
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
@ -310,7 +318,7 @@
// case ResponseComposition.EXTRACT: // case ResponseComposition.EXTRACT:
// return Object.keys(props.response.extract) // return Object.keys(props.response.extract)
// .map((e) => `${e}: ${props.response.extract[e]}`) // .map((e) => `${e}: ${props.response.extract[e]}`)
// .join('\n'); // .join('\n'); // TODO:
default: default:
return ''; return '';
} }

View File

@ -1,876 +0,0 @@
<template>
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
<MsEditableTab
v-model:active-tab="activeRequestTab"
v-model:tabs="debugTabs"
:more-action-list="moreActionList"
at-least-one
@add="addDebugTab"
@close="closeDebugTab"
@change="setActiveDebug"
@more-action-select="handleMoreActionSelect"
>
<template #label="{ tab }">
<apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
{{ tab.label }}
</template>
</MsEditableTab>
</div>
<div class="px-[24px] pt-[16px]">
<div class="mb-[4px] flex items-center justify-between">
<div class="flex flex-1">
<a-select
v-model:model-value="activeDebug.protocol"
:options="protocolOptions"
:loading="protocolLoading"
class="mr-[4px] w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="activeDebug.method"
class="w-[140px]"
@change="handleActiveDebugChange"
/>
<a-input
v-model:model-value="activeDebug.url"
:max-length="255"
:placeholder="t('apiTestDebug.urlPlaceholder')"
@change="handleActiveDebugChange"
/>
</a-input-group>
</div>
<div class="ml-[16px]">
<a-dropdown-button
:button-props="{ loading: executeLoading }"
:disabled="executeLoading"
class="exec-btn"
@click="execute"
@select="execute"
>
{{ isLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isLocalExec ? 'localExec' : 'serverExec'">
{{ isLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button type="secondary" @click="handleSaveShortcut">
<div class="flex items-center">
{{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div>
</div>
</a-button>
</div>
</div>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-125px)]">
<MsSplitBox
ref="splitBoxRef"
v-model:size="splitBoxSize"
:max="0.98"
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
@expand-change="handleExpandChange"
>
<template #first>
<div
:class="`flex h-full min-w-[800px] flex-col px-[24px] pb-[16px] ${
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
}`"
>
<div>
<a-tabs v-model:active-key="activeDebug.activeTab" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider>
</div>
<div class="tab-pane-container">
<template v-if="isInitPluginForm || activeDebug.activeTab === RequestComposition.PLUGIN">
<a-spin v-show="activeDebug.activeTab === RequestComposition.PLUGIN" :loading="pluginLoading">
<MsFormCreate v-model:api="fApi" :rule="currentPluginScript" :option="options" />
</a-spin>
</template>
<debugHeader
v-if="activeDebug.activeTab === RequestComposition.HEADER"
v-model:params="activeDebug.headers"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugBody
v-else-if="activeDebug.activeTab === RequestComposition.BODY"
v-model:params="activeDebug.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugQuery
v-else-if="activeDebug.activeTab === RequestComposition.QUERY"
v-model:params="activeDebug.query"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugRest
v-else-if="activeDebug.activeTab === RequestComposition.REST"
v-model:params="activeDebug.rest"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-model:config="activeDebug.children[0].preProcessorConfig"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-model:config="activeDebug.children[0].postProcessorConfig"
:response="activeDebug.response.requestResults[0]?.responseResult.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="activeDebug.activeTab === RequestComposition.AUTH"
v-model:params="activeDebug.authConfig"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="activeDebug.activeTab === RequestComposition.SETTING"
v-model:params="activeDebug.otherConfig"
@change="handleActiveDebugChange"
/>
</div>
</div>
</template>
<template #second>
<response
v-model:active-layout="activeLayout"
v-model:active-tab="activeDebug.responseActiveTab"
:is-expanded="isExpanded"
:response="activeDebug.response"
@change-expand="changeExpand"
@change-layout="handleActiveLayoutChange"
/>
</template>
</MsSplitBox>
</div>
<a-modal
v-model:visible="saveModalVisible"
:title="t('common.save')"
:ok-loading="saveLoading"
class="ms-modal-form"
title-align="start"
body-class="!p-0"
@before-ok="handleSave"
@cancel="handleCancel"
>
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
<a-form-item
field="name"
:label="t('apiTestDebug.requestName')"
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
</a-form-item>
<a-form-item
v-if="isHttpProtocol"
field="path"
:label="t('apiTestDebug.requestUrl')"
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.path" :placeholder="t('apiTestDebug.commonPlaceholder')" />
</a-form-item>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select
v-model:modelValue="saveModalForm.moduleId"
:data="selectTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
allow-search
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import debugAuth from './auth.vue';
import postcondition from './postcondition.vue';
import precondition from './precondition.vue';
import response from './response.vue';
import debugSetting from './setting.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import { addDebug, executeDebug, getDebugDetail, updateDebug } from '@/api/modules/api-test/debug';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { filterTree, getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { ExecuteBody, ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestMethods,
ResponseComposition,
} from '@/enums/apiEnum';
// Http
const debugHeader = defineAsyncComponent(() => import('./header.vue'));
const debugBody = defineAsyncComponent(() => import('./body.vue'));
const debugQuery = defineAsyncComponent(() => import('./query.vue'));
const debugRest = defineAsyncComponent(() => import('./rest.vue'));
export type DebugTabParam = ExecuteHTTPRequestFullParams & TabItem & Record<string, any>;
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
detailLoading: boolean; //
}>();
const emit = defineEmits(['update:detailLoading', 'addDone']);
const appStore = useAppStore();
const { t } = useI18n();
const loading = useVModel(props, 'detailLoading', emit);
const initDefaultId = `debug-${Date.now()}`;
const activeRequestTab = ref<string | number>(initDefaultId);
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
const defaultResponse = {
requestResults: [
{
body: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
},
},
],
console: '',
}; //
const defaultDebugParams: DebugTabParam = {
id: initDefaultId,
moduleId: 'root',
protocol: 'HTTP',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
userName: '',
password: '',
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
};
const debugTabs = ref<DebugTabParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<DebugTabParam>(debugTabs.value[0]);
const isHttpProtocol = computed(() => activeDebug.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); //
watch(
() => activeDebug.value.protocol,
(val) => {
if (val !== 'HTTP') {
isInitPluginForm.value = true;
}
},
{
immediate: true,
}
);
function setActiveDebug(item: TabItem) {
activeDebug.value = item as DebugTabParam;
}
function handleActiveDebugChange() {
if (!loading.value) {
// change
activeDebug.value.unSaved = true;
}
}
function addDebugTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
id,
isNew: !defaultProps?.id, // tabidid
...defaultProps,
});
activeRequestTab.value = defaultProps?.id || id;
nextTick(() => {
if (defaultProps && !defaultProps.id) {
handleActiveDebugChange();
}
});
}
function closeDebugTab(tab: TabItem) {
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
debugTabs.value.splice(index, 1);
if (activeRequestTab.value === tab.id) {
activeRequestTab.value = debugTabs.value[0]?.id || '';
}
}
const moreActionList = [
{
eventTag: 'closeOther',
label: t('apiTestDebug.closeOther'),
},
];
function handleMoreActionSelect(event: ActionsItem) {
if (event.eventTag === 'closeOther') {
debugTabs.value = debugTabs.value.filter((item) => item.id === activeRequestTab.value);
}
}
// tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.PRECONDITION,
label: t('apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('apiTestDebug.post'),
},
{
value: RequestComposition.ASSERTION,
label: t('apiTestDebug.assertion'),
},
{
value: RequestComposition.AUTH,
label: t('apiTestDebug.auth'),
},
{
value: RequestComposition.SETTING,
label: t('apiTestDebug.setting'),
},
];
// tab
const contentTabList = computed(() =>
isHttpProtocol.value
? httpContentTabList
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]
);
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const isLocalExec = ref(false); //
async function initLocalConfig() {
try {
const res = await getLocalConfig();
isLocalExec.value = res.find((e) => e.type === 'API')?.enable || false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const pluginScriptMap = ref<Record<string, any>>({}); //
const pluginLoading = ref(false);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[activeDebug.value.protocol] || []
);
async function initPluginScript() {
if (pluginScriptMap.value[activeDebug.value.protocol] !== undefined) {
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(
protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.pluginId || ''
);
pluginScriptMap.value[activeDebug.value.protocol] = res.script;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
function handleActiveDebugProtocolChange(val: string) {
if (val !== 'HTTP') {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
initPluginScript();
} else {
activeDebug.value.activeTab = RequestComposition.HEADER;
}
handleActiveDebugChange();
}
const fApi = ref();
const options = {
form: {
labelAlign: 'right',
autoLabelWidth: true,
size: 'small',
hideRequiredAsterisk: false,
showMessage: true,
inlineMessage: false,
scrollToFirstError: true,
},
submitBtn: false,
resetBtn: false,
};
const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0);
watch(
() => splitBoxSize.value,
debounce((val) => {
// 300ms
if (splitContainerRef.value) {
if (typeof val === 'string' && val.includes('px')) {
val = Number(val.split('px')[0]);
secondBoxHeight.value = splitContainerRef.value.clientHeight - val;
} else {
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
}
}
}, 300),
{
immediate: true,
}
);
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isExpanded = ref(true);
function handleExpandChange(val: boolean) {
isExpanded.value = val;
}
function changeExpand(val: boolean) {
isExpanded.value = val;
if (val) {
splitBoxRef.value?.expand(0.6);
} else {
splitBoxRef.value?.collapse(splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0);
}
}
function handleActiveLayoutChange() {
isExpanded.value = true;
splitBoxSize.value = 0.6;
splitBoxRef.value?.expand(0.6);
}
const executeLoading = ref(false);
const reportId = ref('');
const websocket = ref<WebSocket>();
function debugSocket() {
websocket.value = getSocket(reportId.value);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
activeDebug.value.response = data.taskResult;
executeLoading.value = false;
}
});
}
function makeRequestParams() {
const polymorphicName = protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.polymorphicName; //
let requestParams;
if (isHttpProtocol.value) {
requestParams = {
authConfig: activeDebug.value.authConfig,
body: {
...activeDebug.value.body,
binaryBody: undefined,
formDataBody: {
formValues: activeDebug.value.body.formDataBody.formValues.filter(
(e, i) => i !== activeDebug.value.body.formDataBody.formValues.length - 1
), //
},
wwwFormBody: {
formValues: activeDebug.value.body.wwwFormBody.formValues.filter(
(e, i) => i !== activeDebug.value.body.wwwFormBody.formValues.length - 1
), //
},
}, // TODO:binaryBody
headers: activeDebug.value.headers.filter((e, i) => i !== activeDebug.value.headers.length - 1), //
method: activeDebug.value.method,
otherConfig: activeDebug.value.otherConfig,
path: activeDebug.value.url,
query: activeDebug.value.query.filter((e, i) => i !== activeDebug.value.query.length - 1), //
rest: activeDebug.value.rest.filter((e, i) => i !== activeDebug.value.rest.length - 1), //
url: activeDebug.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value.form,
polymorphicName,
};
}
reportId.value = getGenerateId();
debugSocket(); // websocket
return {
id: activeDebug.value.id.toString(),
reportId: reportId.value,
environmentId: '',
tempFileIds: [],
request: {
...requestParams,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
// TODO:
enableGlobal: false,
assertions: [],
},
postProcessorConfig: activeDebug.value.children[0].postProcessorConfig,
preProcessorConfig: activeDebug.value.children[0].preProcessorConfig,
},
],
},
projectId: appStore.currentProjectId,
};
}
/**
* 执行调试
* @param val 执行类型
*/
async function execute(execuetType?: 'localExec' | 'serverExec') {
// TODO:&
if (isHttpProtocol.value) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
//
fApi.value?.validate(async (valid) => {
if (valid === true) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
});
}
}
const saveModalVisible = ref(false);
const saveModalForm = ref({
name: '',
path: activeDebug.value.url || '',
moduleId: 'root',
});
const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false);
const selectTree = computed(() =>
filterTree(cloneDeep(props.moduleTree), (e) => {
e.draggable = false;
return e.type === 'MODULE';
})
);
watch(
() => saveModalVisible.value,
(val) => {
if (!val) {
saveModalFormRef.value?.resetFields();
}
}
);
async function handleSaveShortcut() {
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveModalForm.value = {
name: activeDebug.value.name || '',
path: activeDebug.value.url || '',
moduleId: 'root',
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
}
function handleCancel() {
saveModalFormRef.value?.resetFields();
}
async function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
saveLoading.value = true;
if (activeDebug.value.isNew) {
//
await addDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
} else {
await updateDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
deleteFileIds: [], // TODO:
unLinkRefIds: [], // TODO:
});
}
saveLoading.value = false;
saveModalVisible.value = false;
done(true);
activeDebug.value.unSaved = false;
activeDebug.value.name = saveModalForm.value.name;
activeDebug.value.label = saveModalForm.value.name;
emit('addDone');
Message.success(activeDebug.value.isNew ? t('common.saveSuccess') : t('common.updateSuccess'));
} catch (error) {
saveLoading.value = false;
}
}
});
done(false);
}
async function openApiTab(apiInfo: ModuleTreeNode) {
const isLoadedTabIndex = debugTabs.value.findIndex((e) => e.id === apiInfo.id);
if (isLoadedTabIndex > -1) {
// tabtab
activeRequestTab.value = apiInfo.id;
return;
}
try {
loading.value = true;
const res = await getDebugDetail(apiInfo.id);
addDebugTab({
label: apiInfo.name,
...res,
response: cloneDeep(defaultResponse),
...res.request,
url: res.path,
name: res.name, // requestnamenull
});
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
onBeforeMount(() => {
initProtocolList();
initLocalConfig();
});
onMounted(() => {
registerCatchSaveShortcut(handleSaveShortcut);
});
onBeforeUnmount(() => {
removeCatchSaveShortcut(handleSaveShortcut);
});
defineExpose({
addDebugTab,
openApiTab,
});
</script>
<style lang="less" scoped>
.exec-btn {
margin-right: 12px;
:deep(.arco-btn) {
color: white !important;
background-color: rgb(var(--primary-5)) !important;
.btn-base-primary-hover();
.btn-base-primary-active();
.btn-base-primary-disabled();
}
}
.tab-pane-container {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
}
:deep(.no-content) {
.arco-tabs-content {
display: none;
}
}
</style>

View File

@ -7,20 +7,33 @@
<moduleTree <moduleTree
ref="moduleTreeRef" ref="moduleTreeRef"
@init="(val) => (folderTree = val)" @init="(val) => (folderTree = val)"
@new-api="newApi" @new-api="addDebugTab"
@click-api-node="handleApiNodeClick" @click-api-node="openApiTab"
@import="importDrawerVisible = true" @import="importDrawerVisible = true"
/> />
</div> </div>
</template> </template>
<template #second> <template #second>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<debug <div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
ref="debugRef" <MsEditableTab v-model:active-tab="activeDebug" v-model:tabs="debugTabs" at-least-one @add="addDebugTab">
v-model:detail-loading="loading" <template #label="{ tab }">
:module-tree="folderTree" <apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
@add-done="handleDebugAddDone" {{ tab.label }}
/> </template>
</MsEditableTab>
</div>
<div class="flex-1 overflow-hidden">
<debug
v-model:detail-loading="loading"
v-model:request="activeDebug"
:module-tree="folderTree"
:create-api="addDebug"
:update-api="updateDebug"
:execute-api="executeDebug"
@add-done="handleDebugAddDone"
/>
</div>
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
@ -58,35 +71,194 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import debug from './components/debug/index.vue';
import moduleTree from './components/moduleTree.vue'; import moduleTree from './components/moduleTree.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import debug, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { addDebug, executeDebug, getDebugDetail, updateDebug } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { parseCurlScript } from '@/utils'; import { parseCurlScript } from '@/utils';
import { ExecuteBody } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum'; import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestContentTypeEnum,
RequestMethods,
RequestParamsType,
ResponseComposition,
} from '@/enums/apiEnum';
const { t } = useI18n(); const { t } = useI18n();
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>(); const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const debugRef = ref<InstanceType<typeof debug>>();
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const importDrawerVisible = ref(false); const importDrawerVisible = ref(false);
const curlCode = ref(''); const curlCode = ref('');
const loading = ref(false); const loading = ref(false);
function newApi() { function handleDebugAddDone() {
debugRef.value?.addDebugTab(); moduleTreeRef.value?.initModules();
moduleTreeRef.value?.initModuleCount();
}
const initDefaultId = `debug-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
const defaultResponse = {
requestResults: [
{
body: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
},
},
],
console: '',
}; //
const defaultDebugParams: RequestParam = {
id: initDefaultId,
moduleId: 'root',
protocol: 'HTTP',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
userName: '',
password: '',
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
};
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<RequestParam>(debugTabs.value[0]);
const isHttpProtocol = computed(() => activeDebug.value.protocol === 'HTTP');
function handleActiveDebugChange() {
if (!loading.value) {
// change
activeDebug.value.unSaved = true;
}
}
function addDebugTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
id,
isNew: !defaultProps?.id, // tabidid
...defaultProps,
});
activeDebug.value = debugTabs.value[debugTabs.value.length - 1];
}
async function openApiTab(apiInfo: ModuleTreeNode) {
const isLoadedTabIndex = debugTabs.value.findIndex((e) => e.id === apiInfo.id);
if (isLoadedTabIndex > -1) {
// tabtab
activeDebug.value = debugTabs.value[isLoadedTabIndex];
return;
}
try {
loading.value = true;
const res = await getDebugDetail(apiInfo.id);
addDebugTab({
label: apiInfo.name,
...res,
response: cloneDeep(defaultResponse),
...res.request,
url: res.path,
name: res.name, // requestnamenull
});
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
} }
function handleCurlImportConfirm() { function handleCurlImportConfirm() {
const { url, headers, queryParameters } = parseCurlScript(curlCode.value); const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
debugRef.value?.addDebugTab({ addDebugTab({
url, url,
headers: headers?.map((e) => ({ headers: headers?.map((e) => ({
contentType: RequestContentTypeEnum.TEXT, contentType: RequestContentTypeEnum.TEXT,
@ -107,15 +279,9 @@
}); });
curlCode.value = ''; curlCode.value = '';
importDrawerVisible.value = false; importDrawerVisible.value = false;
} nextTick(() => {
handleActiveDebugChange();
function handleApiNodeClick(node: ModuleTreeNode) { });
debugRef.value?.openApiTab(node);
}
function handleDebugAddDone() {
moduleTreeRef.value?.initModules();
moduleTreeRef.value?.initModuleCount();
} }
</script> </script>

View File

@ -0,0 +1,56 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="props.mode === 'pre' ? t('apiTestManagement.addPreDependency') : t('apiTestManagement.addPostDependency')"
:width="960"
no-content-padding
>
<div v-if="innerVisible" class="flex h-full w-full overflow-hidden px-[16px]">
<moduleTree
class="w-[200px] pt-[16px]"
read-only
@init="(val) => (folderTree = val)"
@folder-node-select="handleNodeSelect"
/>
<a-divider direction="vertical" :margin="16"></a-divider>
<apiTable
:active-module="activeModule"
:offspring-ids="offspringIds"
class="flex-1 overflow-hidden !pl-0 !pr-[16px]"
read-only
/>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import apiTable from './apiTable.vue';
import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n';
import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{
visible: boolean;
mode: 'pre' | 'post'; // pre: post:
}>();
const { t } = useI18n();
const innerVisible = defineModel<boolean>('visible', {
default: false,
});
const folderTree = ref<ModuleTreeNode[]>([]);
const activeModule = ref<string>('all');
const offspringIds = ref<string[]>([]);
function handleNodeSelect(keys: string[], _offspringIds: string[]) {
[activeModule.value] = keys;
offspringIds.value = _offspringIds;
}
</script>
<style lang="less" scoped></style>

View File

@ -1,22 +1,6 @@
<template> <template>
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]"> <div :class="['p-[16px_22px]', props.class]">
<MsEditableTab <div class="mb-[16px] flex items-center justify-between">
v-model:active-tab="activeRequestTab"
v-model:tabs="apiTabs"
:more-action-list="tabMoreActionList"
@add="addDebugTab"
@close="closeDebugTab"
@change="setActiveDebug"
@more-action-select="handleTabMoreActionSelect"
>
<template #label="{ tab }">
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" />
{{ tab.label }}
</template>
</MsEditableTab>
</div>
<div class="p-[16px_22px]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-[8px]"> <div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch> <a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
{{ t('apiTestManagement.showSubdirectory') }} {{ t('apiTestManagement.showSubdirectory') }}
@ -31,7 +15,7 @@
v-model:model-value="checkedEnv" v-model:model-value="checkedEnv"
mode="static" mode="static"
:options="envOptions" :options="envOptions"
class="w-[200px]" class="!w-[150px]"
:search-keys="['label']" :search-keys="['label']"
allow-search allow-search
/> />
@ -53,6 +37,7 @@
<ms-base-table <ms-base-table
v-bind="propsRes" v-bind="propsRes"
:action-config="batchActions" :action-config="batchActions"
:first-column-width="44"
no-disable no-disable
filter-icon-align-left filter-icon-align-left
v-on="propsEvent" v-on="propsEvent"
@ -251,8 +236,6 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
@ -260,10 +243,10 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import moduleTree from '../moduleTree.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
@ -274,79 +257,22 @@
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{ const props = defineProps<{
module: string; class?: string;
allCount: number;
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
readOnly?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'init', params: any): void; (e: 'init', params: any): void;
(e: 'change'): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const activeRequestTab = ref<string | number>('all');
const apiTabs = ref<TabItem[]>([
{
id: 'all',
label: `${t('apiTestManagement.allApi')}(${props.allCount})`,
closable: false,
},
]);
const activeApiTab = ref<TabItem>(apiTabs.value[0]);
function setActiveDebug(item: TabItem) {
activeApiTab.value = item;
}
function handleActiveDebugChange() { function handleActiveDebugChange() {
activeApiTab.value.unSaved = true; emit('change');
}
function addDebugTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
apiTabs.value.push({
module: props.module,
label: t('apiTestDebug.newApi'),
id,
...defaultProps,
});
activeRequestTab.value = id;
nextTick(() => {
if (defaultProps) {
handleActiveDebugChange();
}
});
}
function closeDebugTab(tab: TabItem) {
const index = apiTabs.value.findIndex((item) => item.id === tab.id);
apiTabs.value.splice(index, 1);
if (activeRequestTab.value === tab.id) {
activeRequestTab.value = apiTabs.value[0]?.id || '';
}
}
const tabMoreActionList = [
{
eventTag: 'closeAll',
label: t('apiTestManagement.closeAll'),
},
{
eventTag: 'closeOther',
label: t('apiTestManagement.closeOther'),
},
];
function handleTabMoreActionSelect(event: ActionsItem) {
if (event.eventTag === 'closeOther') {
apiTabs.value = apiTabs.value.filter((item) => item.id === activeRequestTab.value || item.closable === false);
} else if (event.eventTag === 'closeAll') {
apiTabs.value = apiTabs.value.filter((item) => item.id === 'all');
activeRequestTab.value = 'all';
}
} }
const showSubdirectory = ref(false); const showSubdirectory = ref(false);
@ -371,7 +297,7 @@
]); ]);
const keyword = ref(''); const keyword = ref('');
const columns: MsTableColumn = [ let columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'num', dataIndex: 'num',
@ -455,32 +381,38 @@
width: 150, width: 150,
}, },
]; ];
const tableStore = useTableStore(); if (!props.readOnly) {
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer'); const tableStore = useTableStore();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
} else {
columns = columns.filter(
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
);
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
() => () =>
Promise.resolve({ Promise.resolve({
list: [ list: [
{ {
id: 1001, num: 1001,
name: 'asdasdasd', name: 'asdasdasd',
type: RequestMethods.CONNECT, type: RequestMethods.CONNECT,
status: RequestDefinitionStatus.DEBUGGING, status: RequestDefinitionStatus.DEBUGGING,
}, },
{ {
id: 10011, num: 10011,
name: '1123', name: '1123',
type: RequestMethods.OPTIONS, type: RequestMethods.OPTIONS,
status: RequestDefinitionStatus.DEPRECATED, status: RequestDefinitionStatus.DEPRECATED,
}, },
{ {
id: 10012, num: 10012,
name: 'vfd', name: 'vfd',
type: RequestMethods.POST, type: RequestMethods.POST,
status: RequestDefinitionStatus.DONE, status: RequestDefinitionStatus.DONE,
}, },
{ {
id: 10013, num: 10013,
name: 'ccf', name: 'ccf',
type: RequestMethods.DELETE, type: RequestMethods.DELETE,
status: RequestDefinitionStatus.PROCESSING, status: RequestDefinitionStatus.PROCESSING,
@ -489,11 +421,13 @@
total: 0, total: 0,
}), }),
{ {
tableKey: TableKeyEnum.API_TEST, columns: props.readOnly ? columns : [],
showSetting: true, scroll: { x: '100%' },
tableKey: props.readOnly ? undefined : TableKeyEnum.API_TEST,
showSetting: !props.readOnly,
selectable: true, selectable: true,
showSelectAll: true, showSelectAll: !props.readOnly,
draggable: { type: 'handle', width: 32 }, draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
}, },
(item) => ({ (item) => ({
...item, ...item,
@ -720,6 +654,7 @@
resetSelector(); resetSelector();
loadList(); loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
batchUpdateLoading.value = false; batchUpdateLoading.value = false;

View File

@ -0,0 +1,347 @@
<template>
<div class="flex h-full flex-col">
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
<MsEditableTab v-model:active-tab="activeApiTab" v-model:tabs="apiTabs" @add="addApiTab">
<template #label="{ tab }">
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" />
{{ tab.label }}
</template>
</MsEditableTab>
</div>
<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">
<a-tabs default-active-key="definition" animation lazy-load class="ms-api-tab-nav">
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane">
<MsSplitBox :size="0.7" :max="0.9" :min="0.7" direction="horizontal" expand-direction="right">
<template #first>
<requestComposition
v-model:detail-loading="loading"
v-model:request="activeApiTab"
:module-tree="props.moduleTree"
hide-response-layout-swicth
:create-api="addDebug"
:update-api="updateDebug"
:execute-api="executeDebug"
is-definiton
@add-done="emit('addDone')"
/>
</template>
<template #second>
<div class="p-[24px]">
<MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" />
<a-dropdown @select="handleSelect">
<a-button type="outline">
<div class="flex items-center gap-[8px]">
<icon-plus />
{{ t('apiTestManagement.addDependency') }}
</div>
</a-button>
<template #content>
<a-doption value="pre">{{ t('apiTestManagement.preDependency') }}</a-doption>
<a-doption value="post">{{ t('apiTestManagement.postDependency') }}</a-doption>
</template>
</a-dropdown>
</div>
</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>
<template #extra>
<div class="flex items-center gap-[8px] pr-[24px]">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
<template #icon>
<icon-location class="text-[var(--color-text-4)]" />
</template>
</a-button>
<MsSelect
v-model:model-value="checkedEnv"
mode="static"
:options="envOptions"
class="!w-[150px]"
:search-keys="['label']"
allow-search
/>
</div>
</template>
</a-tabs>
</div>
</div>
<addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" />
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsSelect from '@/components/business/ms-select';
import addDependencyDrawer from './addDependencyDrawer.vue';
import apiTable from './apiTable.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { addDebug, executeDebug, getDebugDetail, updateDebug } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteBody } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestMethods,
ResponseComposition,
} from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
// requestComposition
const requestComposition = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/index.vue')
);
const props = defineProps<{
module: string;
allCount: number;
activeModule: string;
offspringIds: string[];
moduleTree: ModuleTreeNode[]; //
}>();
const emit = defineEmits(['addDone']);
const { t } = useI18n();
const apiTabs = ref<RequestParam[]>([
{
id: 'all',
label: `${t('apiTestManagement.allApi')}(${props.allCount})`,
closable: false,
} as RequestParam,
]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
function handleActiveDebugChange() {
if (activeApiTab.value) {
activeApiTab.value.unSaved = true;
}
}
const initDefaultId = `debug-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
const defaultResponse = {
requestResults: [
{
body: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
},
},
],
console: '',
}; //
const defaultDebugParams: RequestParam = {
id: initDefaultId,
moduleId: 'root',
protocol: 'HTTP',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
userName: '',
password: '',
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
};
function addApiTab(defaultProps?: Partial<TabItem>) {
const id = `debug-${Date.now()}`;
apiTabs.value.push({
...defaultDebugParams,
moduleId: props.module,
label: t('apiTestDebug.newApi'),
id,
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
nextTick(() => {
if (defaultProps) {
handleActiveDebugChange();
}
});
}
const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode) {
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
if (isLoadedTabIndex > -1) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return;
}
try {
loading.value = true;
const res = await getDebugDetail(apiInfo.id);
addApiTab({
label: apiInfo.name,
...res,
response: cloneDeep(defaultResponse),
...res.request,
url: res.path,
name: res.name, // requestnamenull
});
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
const checkedEnv = ref('DEV');
const envOptions = ref([
{
label: 'DEV',
value: 'DEV',
},
{
label: 'TEST',
value: 'TEST',
},
{
label: 'PRE',
value: 'PRE',
},
{
label: 'PROD',
value: 'PROD',
},
]);
const fApi = ref();
const options = {
form: {
layout: 'vertical',
labelPosition: 'right',
size: 'small',
labelWidth: '00px',
hideRequiredAsterisk: false,
showMessage: true,
inlineMessage: false,
scrollToFirstError: true,
},
submitBtn: false,
resetBtn: false,
};
const currentApiTemplateRules = [];
const showAddDependencyDrawer = ref(false);
const addDependencyMode = ref<'pre' | 'post'>('pre');
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'pre':
addDependencyMode.value = 'pre';
showAddDependencyDrawer.value = true;
break;
case 'post':
addDependencyMode.value = 'post';
showAddDependencyDrawer.value = true;
break;
default:
break;
}
}
defineExpose({
openApiTab,
addApiTab,
});
</script>
<style lang="less" scoped>
.ms-api-tab-nav {
@apply h-full;
:deep(.arco-tabs-content) {
@apply pt-0;
height: calc(100% - 51px);
.arco-tabs-content-list {
@apply h-full;
.arco-tabs-pane {
@apply h-full;
}
}
}
}
</style>

View File

@ -1,38 +1,68 @@
<template> <template>
<a-tabs v-model:active-key="activeTab" animation lazy-load> <a-tabs v-model:active-key="activeTab" animation lazy-load class="ms-api-tab-nav">
<a-tab-pane key="api" title="API"> <a-tab-pane key="api" title="API" class="ms-api-tab-pane">
<api <api
:active-module="activeModule" ref="apiRef"
:module-tree="props.moduleTree"
:active-module="props.activeModule"
:module="props.module" :module="props.module"
:all-count="props.allCount" :all-count="props.allCount"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="case" title="CASE"> </a-tab-pane> <a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="mock" title="MOCK"> </a-tab-pane> <a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')"> </a-tab-pane> <a-tab-pane key="doc" :title="t('apiTestManagement.doc')" class="ms-api-tab-pane"> </a-tab-pane>
</a-tabs> </a-tabs>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import api from './api.vue'; import api from './api/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{ const props = defineProps<{
module: string; module: string;
allCount: number; allCount: number;
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
moduleTree: ModuleTreeNode[]; //
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const activeTab = ref('api'); const activeTab = ref('api');
const apiRef = ref<InstanceType<typeof api>>();
function newTab(apiInfo?: ModuleTreeNode) {
if (apiInfo) {
apiRef.value?.openApiTab(apiInfo);
} else {
apiRef.value?.addApiTab();
}
}
defineExpose({
newTab,
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.arco-tabs-nav) { .ms-api-tab-nav {
border-bottom: 1px solid var(--color-text-n8); @apply h-full;
:deep(.arco-tabs-content) {
height: calc(100% - 51px);
.arco-tabs-content-list {
@apply h-full;
.arco-tabs-pane {
@apply h-full;
}
}
}
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8);
}
} }
</style> </style>

View File

@ -1,34 +1,34 @@
<template> <template>
<div> <div>
<template v-if="!props.isModal"> <a-select
<a-select v-model:model-value="moduleProtocol"
v-model:model-value="moduleProtocol" :options="moduleProtocolOptions"
:options="moduleProtocolOptions" class="mb-[8px]"
class="mb-[8px]" @change="(val) => handleProtocolChange(val as string)"
@change="(val) => handleProtocolChange(val as string)" />
/> <div class="mb-[8px] flex items-center gap-[8px]">
<div class="mb-[8px] flex items-center gap-[8px]"> <a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear /> <a-dropdown v-if="!props.readOnly" @select="handleSelect">
<a-dropdown @select="handleSelect"> <a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button> <template #content>
<template #content> <a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption> <a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption> </template>
</template> </a-dropdown>
</a-dropdown> </div>
<div class="folder" @click="setActiveFolder('all')">
<div :class="allFolderClass">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div> </div>
<div class="folder" @click="setActiveFolder('all')"> <div class="ml-auto flex items-center">
<div :class="allFolderClass"> <a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" /> <MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div> <MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
<div class="folder-count">({{ allFileCount }})</div> </MsButton>
</div> </a-tooltip>
<div class="ml-auto flex items-center"> <template v-if="!props.readOnly">
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<a-dropdown @select="handleSelect"> <a-dropdown @select="handleSelect">
<MsButton type="icon" class="!mr-0 p-[2px]"> <MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon <MsIcon
@ -45,17 +45,10 @@
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules"> <popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
<span id="addModulePopSpan"></span> <span id="addModulePopSpan"></span>
</popConfirm> </popConfirm>
</div> </template>
</div> </div>
<a-divider class="my-[8px]" /> </div>
</template> <a-divider class="my-[8px]" />
<a-input
v-if="props.isModal"
v-model:model-value="moduleKeyword"
:placeholder="t('apiTestManagement.moveSearchTip')"
allow-clear
class="mb-[16px]"
/>
<a-spin class="min-h-[400px] w-full" :loading="loading"> <a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree <MsTree
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"
@ -73,6 +66,7 @@
children: 'children', children: 'children',
count: 'count', count: 'count',
}" }"
:filter-more-action-func="filterMoreActionFunc"
block-node block-node
title-tooltip-position="left" title-tooltip-position="left"
@select="folderNodeSelect" @select="folderNodeSelect"
@ -81,15 +75,23 @@
@drop="handleDrop" @drop="handleDrop"
> >
<template #title="nodeData"> <template #title="nodeData">
<div class="inline-flex w-full"> <div
v-if="nodeData.type === 'API'"
class="inline-flex w-full cursor-pointer gap-[4px]"
@click="emit('clickApiNode', nodeData)"
>
<apiMethodName :method="nodeData.attachInfo?.method || nodeData.attachInfo?.protocol" />
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div> <div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div v-if="!props.isModal" class="ml-auto text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div> </div>
<div v-else class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div> </div>
</template> </template>
<template v-if="!props.isModal" #extra="nodeData"> <template v-if="!props.readOnly" #extra="nodeData">
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 --> <!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 -->
<popConfirm <popConfirm
v-if="nodeData.id !== 'root'" v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
mode="add" mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')" :all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id" :parent-id="nodeData.id"
@ -120,49 +122,70 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message, SelectOptionData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue'; import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview'; import {
deleteDebugModule,
getDebugModuleCount,
getDebugModules,
moveDebugModule,
} from '@/api/modules/api-test/debug';
import { getProtocolList } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { filterTree, mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modulesCount?: Record<string, number>; //
isExpandAll?: boolean; // isExpandAll?: boolean; //
activeModule?: string | number; // key activeModule?: string | number; // key
isModal?: boolean; // readOnly?: boolean; //
}>(), }>(),
{ {
activeModule: 'all', activeModule: 'all',
} }
); );
const emit = defineEmits(['init', 'change', 'newApi', 'import', 'folderNodeSelect']); const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode']);
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const moduleProtocol = ref('HTTP'); const moduleProtocol = ref('HTTP');
const moduleProtocolOptions = ref([ const moduleProtocolOptions = ref<SelectOptionData[]>([]);
{ const protocolLoading = ref(false);
label: 'HTTP',
value: 'HTTP', async function initProtocolList() {
}, try {
]); protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
moduleProtocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
function handleProtocolChange(value: string | number | Record<string, any>) { function handleProtocolChange(value: string | number | Record<string, any>) {
// TODO:
console.log(value); console.log(value);
} }
@ -183,7 +206,7 @@
} }
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
if (props.isModal) { if (props.readOnly) {
return { return {
height: 'calc(60vh - 190px)', height: 'calc(60vh - 190px)',
}; };
@ -193,7 +216,6 @@
}; };
}); });
const allFileCount = ref(0);
const isExpandAll = ref(props.isExpandAll); const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); // const rootModulesName = ref<string[]>([]); //
@ -217,13 +239,6 @@
); );
const loading = ref(false); const loading = ref(false);
watch(
() => selectedKeys.value,
(arr) => {
emit('change', arr ? arr[0] : '');
}
);
function setActiveFolder(id: string) { function setActiveFolder(id: string) {
selectedKeys.value = [id]; selectedKeys.value = [id];
} }
@ -244,6 +259,10 @@
label: 'apiTestManagement.share', label: 'apiTestManagement.share',
eventTag: 'share', eventTag: 'share',
}, },
{
label: 'apiTestManagement.shareModule',
eventTag: 'shareModule',
},
{ {
isDivider: true, isDivider: true,
}, },
@ -253,8 +272,19 @@
danger: true, danger: true,
}, },
]; ];
const renamePopVisible = ref(false); const moduleActions = folderMoreActions.filter(
(action) => action.eventTag === undefined || !['execute', 'share'].includes(action.eventTag)
);
const apiActions = folderMoreActions.filter((action) => action.eventTag !== 'shareModule');
function filterMoreActionFunc(actions, node) {
if (node.type === 'MODULE') {
return moduleActions;
}
return apiActions;
}
const modulesCount = ref<Record<string, number>>({});
const allFileCount = computed(() => modulesCount.value.all || 0);
/** /**
* 初始化模块树 * 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点 * @param isSetDefaultKey 是否设置第一个节点为选中节点
@ -262,15 +292,29 @@
async function initModules(isSetDefaultKey = false) { async function initModules(isSetDefaultKey = false) {
try { try {
loading.value = true; loading.value = true;
const res = await getReviewModules(appStore.currentProjectId); const res = await getDebugModules();
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => { if (props.readOnly) {
return { folderTree.value = filterTree<ModuleTreeNode>(res, (e) => {
...e, if (e.type === 'MODULE') {
hideMoreAction: e.id === 'root', e = {
draggable: e.id !== 'root' && !props.isModal, ...e,
disabled: e.id === selectedKeys.value[0] && props.isModal, hideMoreAction: true,
}; draggable: false,
}); };
return true;
}
return false;
});
} else {
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.readOnly,
disabled: e.id === selectedKeys.value[0] && props.readOnly,
};
});
}
if (isSetDefaultKey) { if (isSetDefaultKey) {
selectedKeys.value = [folderTree.value[0].id]; selectedKeys.value = [folderTree.value[0].id];
} }
@ -283,16 +327,37 @@
} }
} }
async function initModuleCount() {
try {
const res = await getDebugModuleCount({
keyword: moduleKeyword.value,
});
modulesCount.value = res;
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: res[node.id] || 0,
draggable: node.id !== 'root',
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
/** /**
* 处理文件夹树节点选中事件 * 处理文件夹树节点选中事件
*/ */
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) { function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
const offspringIds: string[] = []; if (node.type === 'MODULE') {
mapTree(node.children || [], (e) => { const offspringIds: string[] = [];
offspringIds.push(e.id); mapTree(node.children || [], (e) => {
return e; offspringIds.push(e.id);
}); return e;
emit('folderNodeSelect', _selectedKeys, offspringIds); });
emit('folderNodeSelect', _selectedKeys, offspringIds);
}
} }
/** /**
@ -311,9 +376,10 @@
maskClosable: false, maskClosable: false,
onBeforeOk: async () => { onBeforeOk: async () => {
try { try {
await deleteReviewModule(node.id); await deleteDebugModule(node.id);
Message.success(t('apiTestDebug.deleteSuccess')); Message.success(t('apiTestDebug.deleteSuccess'));
initModules(); await initModules();
initModuleCount();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -323,6 +389,7 @@
}); });
} }
const renamePopVisible = ref(false);
const renameFolderTitle = ref(''); // const renameFolderTitle = ref(''); //
function resetFocusNodeKey() { function resetFocusNodeKey() {
@ -366,7 +433,7 @@
) { ) {
try { try {
loading.value = true; loading.value = true;
await moveReviewModule({ await moveDebugModule({
dragNodeId: dragNode.id as string, dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '', dropNodeId: dropNode.id || '',
dropPosition, dropPosition,
@ -377,7 +444,8 @@
console.log(error); console.log(error);
} finally { } finally {
loading.value = false; loading.value = false;
initModules(); await initModules();
initModuleCount();
} }
} }
@ -388,27 +456,15 @@
} }
} }
onBeforeMount(() => { onBeforeMount(async () => {
initModules(); initProtocolList();
await initModules();
initModuleCount();
}); });
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
defineExpose({ defineExpose({
initModules, initModules,
initModuleCount,
}); });
</script> </script>

View File

@ -6,9 +6,9 @@
<moduleTree <moduleTree
@init="(val) => (folderTree = val)" @init="(val) => (folderTree = val)"
@new-api="newApi" @new-api="newApi"
@change="(val) => (activeModule = val)"
@import="importDrawerVisible = true" @import="importDrawerVisible = true"
@folder-node-select="(keys, _offspringIds) => (offspringIds = _offspringIds)" @folder-node-select="handleNodeSelect"
@click-api-node="handleApiNodeClick"
/> />
</div> </div>
<!-- <div class="b-0 absolute w-[88%]"> <!-- <div class="b-0 absolute w-[88%]">
@ -36,6 +36,8 @@
/> />
</div> </div>
<management <management
ref="managementRef"
:module-tree="folderTree"
:module="activeModule" :module="activeModule"
:all-count="allCount" :all-count="allCount"
:active-module="activeModule" :active-module="activeModule"
@ -61,9 +63,19 @@
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 managementRef = ref<InstanceType<typeof management>>();
function newApi() { function newApi() {
// debugRef.value?.addDebugTab(); managementRef.value?.newTab();
}
function handleNodeSelect(keys: string[], _offspringIds: string[]) {
[activeModule.value] = keys;
offspringIds.value = _offspringIds;
}
function handleApiNodeClick(node: ModuleTreeNode) {
managementRef.value?.newTab(node);
} }
</script> </script>

View File

@ -10,6 +10,7 @@ export default {
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口', 'apiTestManagement.noMatchModule': '暂无匹配的模块/接口',
'apiTestManagement.execute': '执行', 'apiTestManagement.execute': '执行',
'apiTestManagement.share': '分享 API', 'apiTestManagement.share': '分享 API',
'apiTestManagement.shareModule': '分享模块',
'apiTestManagement.doc': '文档', 'apiTestManagement.doc': '文档',
'apiTestManagement.closeAll': '关闭全部tab', 'apiTestManagement.closeAll': '关闭全部tab',
'apiTestManagement.closeOther': '关闭其他tab', 'apiTestManagement.closeOther': '关闭其他tab',
@ -72,4 +73,13 @@ export default {
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)', 'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
'apiTestManagement.timeTaskDay': '(每天)', 'apiTestManagement.timeTaskDay': '(每天)',
'apiTestManagement.customFrequency': '自定义频率', 'apiTestManagement.customFrequency': '自定义频率',
'apiTestManagement.case': '用例',
'apiTestManagement.definition': '定义',
'apiTestManagement.addDependency': '添加依赖关系',
'apiTestManagement.preDependency': '前置依赖',
'apiTestManagement.addPreDependency': '添加前置依赖',
'apiTestManagement.postDependency': '后置依赖',
'apiTestManagement.addPostDependency': '添加后置依赖',
'apiTestManagement.saveAsCase': '保存为新用例',
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
}; };

View File

@ -1,11 +1,11 @@
import { Language } from '@/components/pure/ms-code-editor/types';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript'; import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
const { t } = useI18n(); const { t } = useI18n();
export type Languages = 'groovy' | 'python' | 'beanshell' | 'nashornScript' | 'rhinoScript' | 'javascript';
export const SCRIPT_MENU: CommonScriptMenu[] = [ export const SCRIPT_MENU: CommonScriptMenu[] = [
{ {
title: t('project.code_segment.importApiTest'), title: t('project.code_segment.importApiTest'),
@ -505,7 +505,7 @@ function jsCode(requestObj) {
return _jsTemplate(requestObj); return _jsTemplate(requestObj);
} }
export function getCodeTemplate(language: Languages, requestObj: any) { export function getCodeTemplate(language: Language, requestObj: any) {
switch (language) { switch (language) {
case 'groovy': case 'groovy':
return groovyCode(requestObj); return groovyCode(requestObj);

View File

@ -1,9 +1,9 @@
<template> <template>
<PostTab v-model:params="params" layout="horizontal" /> <PostTab v-model:config="params" layout="horizontal" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import PostTab from '@/views/api-test/debug/components/debug/postcondition.vue'; import PostTab from '@/views/api-test/components/requestComposition/postcondition.vue';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore'; import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';

View File

@ -1,9 +1,9 @@
<template> <template>
<PreTab v-model:params="params" layout="horizontal" /> <PreTab v-model:config="params" layout="horizontal" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import PreTab from '@/views/api-test/debug/components/debug/precondition.vue'; import PreTab from '@/views/api-test/components/requestComposition/precondition.vue';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore'; import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';