mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-29 18:48:13 +08:00
feat(接口测试):接口管理页面-50%
This commit is contained in:
parent
eb56b30e9d
commit
8f7f3b2b08
@ -627,6 +627,9 @@
|
||||
}
|
||||
|
||||
/** 开关 **/
|
||||
.arco-switch {
|
||||
margin-left: 2px; // 避免开关圆形左边被遮挡
|
||||
}
|
||||
.arco-switch-type-line.arco-switch-small {
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
|
@ -43,27 +43,29 @@
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import { Language } from '@/components/pure/ms-code-editor/types';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
||||
|
||||
import type { CommonScriptMenu } from './types';
|
||||
import { getCodeTemplate, type Languages, SCRIPT_MENU } from './utils';
|
||||
import { getCodeTemplate, SCRIPT_MENU } from './utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
expand: boolean;
|
||||
languagesType: Languages | RequestConditionScriptLanguage;
|
||||
languagesType: Language | RequestConditionScriptLanguage;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:expand', value: boolean): void;
|
||||
(e: 'update:languagesType', value: Languages): void;
|
||||
(e: 'update:languagesType', value: Language): void;
|
||||
(e: 'insert', code: string): void;
|
||||
(e: 'formApiImport'): void; // 从api 定义导入
|
||||
(e: 'insertCommonScript'): void; // 从api 定义导入
|
||||
(e: 'updateLanguages', value: Languages): void; // 从api 定义导入
|
||||
(e: 'updateLanguages', value: Language): void; // 从api 定义导入
|
||||
}>();
|
||||
|
||||
const innerExpand = useVModel(props, 'expand', emit);
|
||||
@ -144,7 +146,7 @@
|
||||
function changeHandler(
|
||||
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
|
||||
) {
|
||||
innerLanguageType.value = value as Languages;
|
||||
innerLanguageType.value = value as Language;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -88,14 +88,12 @@
|
||||
import type { CommonScriptItem } from '@/models/projectManagement/commonScript';
|
||||
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
||||
|
||||
import { type Languages } from './utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showType: 'commonScript' | 'executionResult'; // 执行类型
|
||||
language: Languages | RequestConditionScriptLanguage;
|
||||
language: Language | RequestConditionScriptLanguage;
|
||||
code: string;
|
||||
enableRadioSelected?: boolean;
|
||||
executionResult?: string; // 执行结果
|
||||
@ -106,7 +104,7 @@
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:language', value: Languages | RequestConditionScriptLanguage): void;
|
||||
(e: 'update:language', value: Language | RequestConditionScriptLanguage): void;
|
||||
(e: 'update:code', value: string): void;
|
||||
}>();
|
||||
|
||||
|
@ -157,22 +157,23 @@
|
||||
async function testApi() {
|
||||
try {
|
||||
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);
|
||||
apiConfig.value.status = res ? 1 : 2;
|
||||
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'));
|
||||
} else {
|
||||
Message.error(t('ms.personal.testFail'));
|
||||
|
@ -35,7 +35,11 @@
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
:list="props.nodeMoreActions"
|
||||
:list="
|
||||
typeof props.filterMoreActionFunc === 'function'
|
||||
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
|
||||
: props.nodeMoreActions
|
||||
"
|
||||
trigger="click"
|
||||
@select="handleNodeMoreSelect($event, _props)"
|
||||
@close="moreActionsClose"
|
||||
@ -112,6 +116,7 @@
|
||||
| 'right'
|
||||
| 'rt'
|
||||
| 'rb'; // 标题 tooltip 的位置
|
||||
filterMoreActionFunc?: (items: ActionsItem[], node: MsTreeNodeData) => ActionsItem[]; // 过滤更多操作按钮
|
||||
}>(),
|
||||
{
|
||||
searchDebounce: 300,
|
||||
|
@ -16,7 +16,7 @@
|
||||
v-for="tab in props.tabs"
|
||||
:key="tab.id"
|
||||
class="ms-editable-tab"
|
||||
:class="{ active: innerActiveTab === tab.id }"
|
||||
:class="{ active: innerActiveTab?.id === tab.id }"
|
||||
@click="handleTabClick(tab)"
|
||||
>
|
||||
<div :draggable="!!tab.draggable" class="flex items-center">
|
||||
@ -46,6 +46,7 @@
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="!props.readonly"
|
||||
:content="t('ms.editableTab.limitTip', { max: props.limit })"
|
||||
:disabled="!props.limit || props.tabs.length >= props.limit"
|
||||
>
|
||||
@ -60,9 +61,9 @@
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<MsMoreAction
|
||||
v-if="props.moreActionList"
|
||||
:list="props.moreActionList"
|
||||
@select="(val) => emit('moreActionSelect', val)"
|
||||
v-if="!props.hideMoreAction && !props.readonly"
|
||||
:list="mergedMoreActionList"
|
||||
@select="handleMoreActionSelect"
|
||||
>
|
||||
<MsButton type="icon" status="secondary" class="ms-editable-tab-button">
|
||||
<MsIcon type="icon-icon_more_outlined" />
|
||||
@ -82,19 +83,22 @@
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
|
||||
import type { TabItem } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: TabItem[];
|
||||
activeTab: string | number;
|
||||
activeTab?: TabItem;
|
||||
moreActionList?: ActionsItem[];
|
||||
limit?: number; // 最多可打开的tab数量
|
||||
atLeastOne?: boolean; // 是否至少保留一个tab
|
||||
hideMoreAction?: boolean; // 是否隐藏更多操作
|
||||
readonly?: boolean; // 是否只读
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:tabs', activeTab: string | number): void;
|
||||
(e: 'update:activeTab', activeTab: string | number): void;
|
||||
(e: 'update:tabs', tabs: TabItem[]): void;
|
||||
(e: 'update:activeTab', activeTab: TabItem): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'close', item: TabItem): void;
|
||||
(e: 'change', item: TabItem): void;
|
||||
@ -102,10 +106,11 @@
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
||||
const innerTabs = useVModel(props, 'tabs', emit);
|
||||
const tabNav = ref<HTMLElement | null>(null);
|
||||
const tabNav = ref<HTMLElement>();
|
||||
const { arrivedState } = useScroll(tabNav);
|
||||
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
||||
|
||||
@ -129,7 +134,7 @@
|
||||
};
|
||||
|
||||
const scrollToActiveTab = () => {
|
||||
const activeTabDom = tabNav.value?.querySelector('.tab.active');
|
||||
const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active');
|
||||
if (activeTabDom) {
|
||||
const tabRect = activeTabDom.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(
|
||||
() => 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(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
scrollToActiveTab();
|
||||
@ -168,16 +186,75 @@
|
||||
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) {
|
||||
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) {
|
||||
emit('change', item);
|
||||
innerActiveTab.value = item.id;
|
||||
innerActiveTab.value = item;
|
||||
nextTick(() => {
|
||||
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>
|
||||
|
||||
|
@ -2,4 +2,10 @@ export default {
|
||||
'ms.editableTab.arrivedLeft': 'Already reached the far left~',
|
||||
'ms.editableTab.arrivedRight': 'Already reached the far right~',
|
||||
'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',
|
||||
};
|
||||
|
@ -2,4 +2,8 @@ export default {
|
||||
'ms.editableTab.arrivedLeft': '到最左侧啦~',
|
||||
'ms.editableTab.arrivedRight': '到最右侧啦~',
|
||||
'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页',
|
||||
'ms.editableTab.closeTabTip': '该标签页有改动的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
|
||||
'ms.editableTab.batchCloseTabTip': '有标签页的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
|
||||
'ms.editableTab.closeAll': '关闭全部',
|
||||
'ms.editableTab.closeOther': '关闭其他',
|
||||
};
|
||||
|
@ -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 {
|
||||
title: string;
|
||||
@ -13,7 +13,7 @@ export interface CommonScriptItem {
|
||||
name: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
type: Languages; // 脚本语言类型
|
||||
type: Language; // 脚本语言类型
|
||||
status: string; // 脚本状态(进行中/已完成)
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
@ -29,7 +29,7 @@ export interface AddOrUpdateCommonScript {
|
||||
id?: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
type: Languages;
|
||||
type: Language;
|
||||
status: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
|
@ -226,16 +226,16 @@ export function filterTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
filterFn: (node: TreeNode<T>) => boolean,
|
||||
customChildrenKey = 'children'
|
||||
): TreeNode<T>[] {
|
||||
): T[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
tree = [tree];
|
||||
}
|
||||
const filteredTree: TreeNode<T>[] = [];
|
||||
const filteredTree: T[] = [];
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
|
||||
if (filterFn(node)) {
|
||||
const newNode: TreeNode<T> = { ...node };
|
||||
const newNode: T = { ...node };
|
||||
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
|
||||
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
|
||||
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);
|
||||
|
@ -8,7 +8,7 @@
|
||||
</a-radio-group>
|
||||
<div
|
||||
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)]">
|
||||
<a-input
|
||||
@ -84,13 +84,15 @@
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<MsScriptDefined
|
||||
v-if="condition.script !== undefined && condition.scriptLanguage !== undefined"
|
||||
v-model:code="condition.script"
|
||||
v-model:language="condition.scriptLanguage"
|
||||
show-type="commonScript"
|
||||
:show-header="false"
|
||||
></MsScriptDefined>
|
||||
<div class="h-[calc(100%-24px)] min-h-[300px]">
|
||||
<MsScriptDefined
|
||||
v-if="condition.script !== undefined && condition.scriptLanguage !== undefined"
|
||||
v-model:code="condition.script"
|
||||
v-model:language="condition.scriptLanguage"
|
||||
show-type="commonScript"
|
||||
:show-header="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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]">
|
||||
@ -443,11 +445,19 @@ org.apache.http.client.method . . . '' at line number 2
|
||||
columns,
|
||||
noDisable: true,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => condition.value.params,
|
||||
(arr) => {
|
||||
propsRes.value.data = arr as any[]; // 查看详情的时候需要赋值一下
|
||||
}
|
||||
);
|
||||
|
||||
const showQuoteDrawer = ref(false);
|
||||
function saveQuoteScriptHandler(item: any) {
|
||||
condition.value.script = item.script;
|
||||
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) => {
|
||||
return {
|
||||
key: e.name,
|
||||
@ -690,7 +700,7 @@ org.apache.http.client.method . . . '' at line number 2
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
.condition-content {
|
||||
@apply flex-1 overflow-y-auto;
|
||||
@apply flex flex-1 flex-col overflow-y-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 16px;
|
||||
|
@ -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 = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
|
||||
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) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
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>
|
@ -23,6 +23,7 @@
|
||||
</template>
|
||||
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||
<a-radio-group
|
||||
v-if="!props.hideLayoutSwicth"
|
||||
v-model:model-value="innerLayout"
|
||||
type="button"
|
||||
size="small"
|
||||
@ -183,12 +184,19 @@
|
||||
console: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
activeTab: keyof typeof ResponseComposition;
|
||||
activeLayout: Direction;
|
||||
isExpanded: boolean;
|
||||
response: Response;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activeTab: keyof typeof ResponseComposition;
|
||||
activeLayout?: Direction;
|
||||
isExpanded: boolean;
|
||||
response: Response;
|
||||
hideLayoutSwicth?: boolean; // 隐藏布局切换
|
||||
}>(),
|
||||
{
|
||||
activeLayout: 'vertical',
|
||||
hideLayoutSwicth: false,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:activeLayout', value: Direction): void;
|
||||
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
|
||||
@ -285,7 +293,7 @@
|
||||
// {
|
||||
// label: t('apiTestDebug.assertion'),
|
||||
// value: ResponseComposition.ASSERTION,
|
||||
// },
|
||||
// }, // TODO:断言暂时没加
|
||||
];
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
@ -310,7 +318,7 @@
|
||||
// case ResponseComposition.EXTRACT:
|
||||
// return Object.keys(props.response.extract)
|
||||
// .map((e) => `${e}: ${props.response.extract[e]}`)
|
||||
// .join('\n');
|
||||
// .join('\n'); // TODO:断言暂时没加
|
||||
default:
|
||||
return '';
|
||||
}
|
@ -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, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
||||
...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) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
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, // request里面还有个name但是是null
|
||||
});
|
||||
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>
|
@ -7,20 +7,33 @@
|
||||
<moduleTree
|
||||
ref="moduleTreeRef"
|
||||
@init="(val) => (folderTree = val)"
|
||||
@new-api="newApi"
|
||||
@click-api-node="handleApiNodeClick"
|
||||
@new-api="addDebugTab"
|
||||
@click-api-node="openApiTab"
|
||||
@import="importDrawerVisible = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #second>
|
||||
<div class="flex h-full flex-col">
|
||||
<debug
|
||||
ref="debugRef"
|
||||
v-model:detail-loading="loading"
|
||||
:module-tree="folderTree"
|
||||
@add-done="handleDebugAddDone"
|
||||
/>
|
||||
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
|
||||
<MsEditableTab v-model:active-tab="activeDebug" v-model:tabs="debugTabs" at-least-one @add="addDebugTab">
|
||||
<template #label="{ tab }">
|
||||
<apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
|
||||
{{ 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>
|
||||
</template>
|
||||
</MsSplitBox>
|
||||
@ -58,35 +71,194 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/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 debug from './components/debug/index.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 { parseCurlScript } from '@/utils';
|
||||
|
||||
import { ExecuteBody } from '@/models/apiTest/debug';
|
||||
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 moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
|
||||
const debugRef = ref<InstanceType<typeof debug>>();
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
const importDrawerVisible = ref(false);
|
||||
const curlCode = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
function newApi() {
|
||||
debugRef.value?.addDebugTab();
|
||||
function handleDebugAddDone() {
|
||||
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, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
||||
...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) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
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, // request里面还有个name但是是null
|
||||
});
|
||||
nextTick(() => {
|
||||
// 等待内容渲染出来再隐藏loading
|
||||
loading.value = false;
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurlImportConfirm() {
|
||||
const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
|
||||
debugRef.value?.addDebugTab({
|
||||
addDebugTab({
|
||||
url,
|
||||
headers: headers?.map((e) => ({
|
||||
contentType: RequestContentTypeEnum.TEXT,
|
||||
@ -107,15 +279,9 @@
|
||||
});
|
||||
curlCode.value = '';
|
||||
importDrawerVisible.value = false;
|
||||
}
|
||||
|
||||
function handleApiNodeClick(node: ModuleTreeNode) {
|
||||
debugRef.value?.openApiTab(node);
|
||||
}
|
||||
|
||||
function handleDebugAddDone() {
|
||||
moduleTreeRef.value?.initModules();
|
||||
moduleTreeRef.value?.initModuleCount();
|
||||
nextTick(() => {
|
||||
handleActiveDebugChange();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -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>
|
@ -1,22 +1,6 @@
|
||||
<template>
|
||||
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
|
||||
<MsEditableTab
|
||||
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="['p-[16px_22px]', props.class]">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
|
||||
{{ t('apiTestManagement.showSubdirectory') }}
|
||||
@ -31,7 +15,7 @@
|
||||
v-model:model-value="checkedEnv"
|
||||
mode="static"
|
||||
:options="envOptions"
|
||||
class="w-[200px]"
|
||||
class="!w-[150px]"
|
||||
:search-keys="['label']"
|
||||
allow-search
|
||||
/>
|
||||
@ -53,6 +37,7 @@
|
||||
<ms-base-table
|
||||
v-bind="propsRes"
|
||||
:action-config="batchActions"
|
||||
:first-column-width="44"
|
||||
no-disable
|
||||
filter-icon-align-left
|
||||
v-on="propsEvent"
|
||||
@ -251,8 +236,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
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 type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
@ -260,10 +243,10 @@
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import moduleTree from '../moduleTree.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.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 useModal from '@/hooks/useModal';
|
||||
@ -274,79 +257,22 @@
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
module: string;
|
||||
allCount: number;
|
||||
class?: string;
|
||||
activeModule: string;
|
||||
offspringIds: string[];
|
||||
readOnly?: boolean; // 是否是只读模式
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'init', params: any): void;
|
||||
(e: 'change'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
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() {
|
||||
activeApiTab.value.unSaved = true;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
emit('change');
|
||||
}
|
||||
|
||||
const showSubdirectory = ref(false);
|
||||
@ -371,7 +297,7 @@
|
||||
]);
|
||||
const keyword = ref('');
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
let columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'num',
|
||||
@ -455,32 +381,38 @@
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
const tableStore = useTableStore();
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
||||
if (!props.readOnly) {
|
||||
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(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
list: [
|
||||
{
|
||||
id: 1001,
|
||||
num: 1001,
|
||||
name: 'asdasdasd',
|
||||
type: RequestMethods.CONNECT,
|
||||
status: RequestDefinitionStatus.DEBUGGING,
|
||||
},
|
||||
{
|
||||
id: 10011,
|
||||
num: 10011,
|
||||
name: '1123',
|
||||
type: RequestMethods.OPTIONS,
|
||||
status: RequestDefinitionStatus.DEPRECATED,
|
||||
},
|
||||
{
|
||||
id: 10012,
|
||||
num: 10012,
|
||||
name: 'vfd',
|
||||
type: RequestMethods.POST,
|
||||
status: RequestDefinitionStatus.DONE,
|
||||
},
|
||||
{
|
||||
id: 10013,
|
||||
num: 10013,
|
||||
name: 'ccf',
|
||||
type: RequestMethods.DELETE,
|
||||
status: RequestDefinitionStatus.PROCESSING,
|
||||
@ -489,11 +421,13 @@
|
||||
total: 0,
|
||||
}),
|
||||
{
|
||||
tableKey: TableKeyEnum.API_TEST,
|
||||
showSetting: true,
|
||||
columns: props.readOnly ? columns : [],
|
||||
scroll: { x: '100%' },
|
||||
tableKey: props.readOnly ? undefined : TableKeyEnum.API_TEST,
|
||||
showSetting: !props.readOnly,
|
||||
selectable: true,
|
||||
showSelectAll: true,
|
||||
draggable: { type: 'handle', width: 32 },
|
||||
showSelectAll: !props.readOnly,
|
||||
draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
|
||||
},
|
||||
(item) => ({
|
||||
...item,
|
||||
@ -720,6 +654,7 @@
|
||||
resetSelector();
|
||||
loadList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
batchUpdateLoading.value = false;
|
@ -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) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
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, // request里面还有个name但是是null
|
||||
});
|
||||
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>
|
@ -1,38 +1,68 @@
|
||||
<template>
|
||||
<a-tabs v-model:active-key="activeTab" animation lazy-load>
|
||||
<a-tab-pane key="api" title="API">
|
||||
<a-tabs v-model:active-key="activeTab" animation lazy-load class="ms-api-tab-nav">
|
||||
<a-tab-pane key="api" title="API" class="ms-api-tab-pane">
|
||||
<api
|
||||
:active-module="activeModule"
|
||||
ref="apiRef"
|
||||
:module-tree="props.moduleTree"
|
||||
:active-module="props.activeModule"
|
||||
:module="props.module"
|
||||
:all-count="props.allCount"
|
||||
:offspring-ids="props.offspringIds"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="case" title="CASE"> </a-tab-pane>
|
||||
<a-tab-pane key="mock" title="MOCK"> </a-tab-pane>
|
||||
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')"> </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" class="ms-api-tab-pane"> </a-tab-pane>
|
||||
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')" class="ms-api-tab-pane"> </a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from './api.vue';
|
||||
import api from './api/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
|
||||
const props = defineProps<{
|
||||
module: string;
|
||||
allCount: number;
|
||||
activeModule: string;
|
||||
offspringIds: string[];
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-tabs-nav) {
|
||||
border-bottom: 1px solid var(--color-text-n8);
|
||||
.ms-api-tab-nav {
|
||||
@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>
|
||||
|
@ -1,34 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="!props.isModal">
|
||||
<a-select
|
||||
v-model:model-value="moduleProtocol"
|
||||
:options="moduleProtocolOptions"
|
||||
class="mb-[8px]"
|
||||
@change="(val) => handleProtocolChange(val as string)"
|
||||
/>
|
||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||
<a-dropdown @select="handleSelect">
|
||||
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-select
|
||||
v-model:model-value="moduleProtocol"
|
||||
:options="moduleProtocolOptions"
|
||||
class="mb-[8px]"
|
||||
@change="(val) => handleProtocolChange(val as string)"
|
||||
/>
|
||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||
<a-dropdown v-if="!props.readOnly" @select="handleSelect">
|
||||
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
||||
</template>
|
||||
</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 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 class="ml-auto flex items-center">
|
||||
<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>
|
||||
<div class="ml-auto flex items-center">
|
||||
<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>
|
||||
<template v-if="!props.readOnly">
|
||||
<a-dropdown @select="handleSelect">
|
||||
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||
<MsIcon
|
||||
@ -45,17 +45,10 @@
|
||||
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
|
||||
<span id="addModulePopSpan"></span>
|
||||
</popConfirm>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
</template>
|
||||
<a-input
|
||||
v-if="props.isModal"
|
||||
v-model:model-value="moduleKeyword"
|
||||
:placeholder="t('apiTestManagement.moveSearchTip')"
|
||||
allow-clear
|
||||
class="mb-[16px]"
|
||||
/>
|
||||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
||||
<MsTree
|
||||
v-model:focus-node-key="focusNodeKey"
|
||||
@ -73,6 +66,7 @@
|
||||
children: 'children',
|
||||
count: 'count',
|
||||
}"
|
||||
:filter-more-action-func="filterMoreActionFunc"
|
||||
block-node
|
||||
title-tooltip-position="left"
|
||||
@select="folderNodeSelect"
|
||||
@ -81,15 +75,23 @@
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<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 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>
|
||||
</template>
|
||||
<template v-if="!props.isModal" #extra="nodeData">
|
||||
<template v-if="!props.readOnly" #extra="nodeData">
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
|
||||
mode="add"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:parent-id="nodeData.id"
|
||||
@ -120,49 +122,70 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||
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 { 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 useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { mapTree } from '@/utils';
|
||||
import { filterTree, mapTree } from '@/utils';
|
||||
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||
isExpandAll?: boolean; // 是否展开所有节点
|
||||
activeModule?: string | number; // 选中的节点 key
|
||||
isModal?: boolean; // 是否是弹窗模式
|
||||
readOnly?: boolean; // 是否是只读模式
|
||||
}>(),
|
||||
{
|
||||
activeModule: 'all',
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(['init', 'change', 'newApi', 'import', 'folderNodeSelect']);
|
||||
const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode']);
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const moduleProtocol = ref('HTTP');
|
||||
const moduleProtocolOptions = ref([
|
||||
{
|
||||
label: 'HTTP',
|
||||
value: 'HTTP',
|
||||
},
|
||||
]);
|
||||
const moduleProtocolOptions = ref<SelectOptionData[]>([]);
|
||||
const protocolLoading = ref(false);
|
||||
|
||||
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>) {
|
||||
// TODO:搜索
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
@ -183,7 +206,7 @@
|
||||
}
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
if (props.isModal) {
|
||||
if (props.readOnly) {
|
||||
return {
|
||||
height: 'calc(60vh - 190px)',
|
||||
};
|
||||
@ -193,7 +216,6 @@
|
||||
};
|
||||
});
|
||||
|
||||
const allFileCount = ref(0);
|
||||
const isExpandAll = ref(props.isExpandAll);
|
||||
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
||||
|
||||
@ -217,13 +239,6 @@
|
||||
);
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => selectedKeys.value,
|
||||
(arr) => {
|
||||
emit('change', arr ? arr[0] : '');
|
||||
}
|
||||
);
|
||||
|
||||
function setActiveFolder(id: string) {
|
||||
selectedKeys.value = [id];
|
||||
}
|
||||
@ -244,6 +259,10 @@
|
||||
label: 'apiTestManagement.share',
|
||||
eventTag: 'share',
|
||||
},
|
||||
{
|
||||
label: 'apiTestManagement.shareModule',
|
||||
eventTag: 'shareModule',
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
@ -253,8 +272,19 @@
|
||||
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 是否设置第一个节点为选中节点
|
||||
@ -262,15 +292,29 @@
|
||||
async function initModules(isSetDefaultKey = false) {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getReviewModules(appStore.currentProjectId);
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
||||
return {
|
||||
...e,
|
||||
hideMoreAction: e.id === 'root',
|
||||
draggable: e.id !== 'root' && !props.isModal,
|
||||
disabled: e.id === selectedKeys.value[0] && props.isModal,
|
||||
};
|
||||
});
|
||||
const res = await getDebugModules();
|
||||
if (props.readOnly) {
|
||||
folderTree.value = filterTree<ModuleTreeNode>(res, (e) => {
|
||||
if (e.type === 'MODULE') {
|
||||
e = {
|
||||
...e,
|
||||
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) {
|
||||
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) {
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(node.children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||
if (node.type === 'MODULE') {
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(node.children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,9 +376,10 @@
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
await deleteReviewModule(node.id);
|
||||
await deleteDebugModule(node.id);
|
||||
Message.success(t('apiTestDebug.deleteSuccess'));
|
||||
initModules();
|
||||
await initModules();
|
||||
initModuleCount();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
@ -323,6 +389,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
const renamePopVisible = ref(false);
|
||||
const renameFolderTitle = ref(''); // 重命名的文件夹名称
|
||||
|
||||
function resetFocusNodeKey() {
|
||||
@ -366,7 +433,7 @@
|
||||
) {
|
||||
try {
|
||||
loading.value = true;
|
||||
await moveReviewModule({
|
||||
await moveDebugModule({
|
||||
dragNodeId: dragNode.id as string,
|
||||
dropNodeId: dropNode.id || '',
|
||||
dropPosition,
|
||||
@ -377,7 +444,8 @@
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
initModules();
|
||||
await initModules();
|
||||
initModuleCount();
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,27 +456,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initModules();
|
||||
onBeforeMount(async () => {
|
||||
initProtocolList();
|
||||
await initModules();
|
||||
initModuleCount();
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化模块文件数量
|
||||
*/
|
||||
watch(
|
||||
() => props.modulesCount,
|
||||
(obj) => {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||
return {
|
||||
...node,
|
||||
count: obj?.[node.id] || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
initModules,
|
||||
initModuleCount,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
<moduleTree
|
||||
@init="(val) => (folderTree = val)"
|
||||
@new-api="newApi"
|
||||
@change="(val) => (activeModule = val)"
|
||||
@import="importDrawerVisible = true"
|
||||
@folder-node-select="(keys, _offspringIds) => (offspringIds = _offspringIds)"
|
||||
@folder-node-select="handleNodeSelect"
|
||||
@click-api-node="handleApiNodeClick"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="b-0 absolute w-[88%]">
|
||||
@ -36,6 +36,8 @@
|
||||
/>
|
||||
</div>
|
||||
<management
|
||||
ref="managementRef"
|
||||
:module-tree="folderTree"
|
||||
:module="activeModule"
|
||||
:all-count="allCount"
|
||||
:active-module="activeModule"
|
||||
@ -61,9 +63,19 @@
|
||||
const allCount = ref(0);
|
||||
const importDrawerVisible = ref(false);
|
||||
const offspringIds = ref<string[]>([]);
|
||||
const managementRef = ref<InstanceType<typeof management>>();
|
||||
|
||||
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>
|
||||
|
||||
|
@ -10,6 +10,7 @@ export default {
|
||||
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口',
|
||||
'apiTestManagement.execute': '执行',
|
||||
'apiTestManagement.share': '分享 API',
|
||||
'apiTestManagement.shareModule': '分享模块',
|
||||
'apiTestManagement.doc': '文档',
|
||||
'apiTestManagement.closeAll': '关闭全部tab',
|
||||
'apiTestManagement.closeOther': '关闭其他tab',
|
||||
@ -72,4 +73,13 @@ export default {
|
||||
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
|
||||
'apiTestManagement.timeTaskDay': '(每天)',
|
||||
'apiTestManagement.customFrequency': '自定义频率',
|
||||
'apiTestManagement.case': '用例',
|
||||
'apiTestManagement.definition': '定义',
|
||||
'apiTestManagement.addDependency': '添加依赖关系',
|
||||
'apiTestManagement.preDependency': '前置依赖',
|
||||
'apiTestManagement.addPreDependency': '添加前置依赖',
|
||||
'apiTestManagement.postDependency': '后置依赖',
|
||||
'apiTestManagement.addPostDependency': '添加后置依赖',
|
||||
'apiTestManagement.saveAsCase': '保存为新用例',
|
||||
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Language } from '@/components/pure/ms-code-editor/types';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
export type Languages = 'groovy' | 'python' | 'beanshell' | 'nashornScript' | 'rhinoScript' | 'javascript';
|
||||
|
||||
export const SCRIPT_MENU: CommonScriptMenu[] = [
|
||||
{
|
||||
title: t('project.code_segment.importApiTest'),
|
||||
@ -505,7 +505,7 @@ function jsCode(requestObj) {
|
||||
return _jsTemplate(requestObj);
|
||||
}
|
||||
|
||||
export function getCodeTemplate(language: Languages, requestObj: any) {
|
||||
export function getCodeTemplate(language: Language, requestObj: any) {
|
||||
switch (language) {
|
||||
case 'groovy':
|
||||
return groovyCode(requestObj);
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<PostTab v-model:params="params" layout="horizontal" />
|
||||
<PostTab v-model:config="params" layout="horizontal" />
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<PreTab v-model:params="params" layout="horizontal" />
|
||||
<PreTab v-model:config="params" layout="horizontal" />
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user