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 {
|
.arco-switch-type-line.arco-switch-small {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
|
@ -43,27 +43,29 @@
|
|||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import { Language } from '@/components/pure/ms-code-editor/types';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
||||||
|
|
||||||
import type { CommonScriptMenu } from './types';
|
import type { CommonScriptMenu } from './types';
|
||||||
import { getCodeTemplate, type Languages, SCRIPT_MENU } from './utils';
|
import { getCodeTemplate, SCRIPT_MENU } from './utils';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
expand: boolean;
|
expand: boolean;
|
||||||
languagesType: Languages | RequestConditionScriptLanguage;
|
languagesType: Language | RequestConditionScriptLanguage;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:expand', value: boolean): void;
|
(e: 'update:expand', value: boolean): void;
|
||||||
(e: 'update:languagesType', value: Languages): void;
|
(e: 'update:languagesType', value: Language): void;
|
||||||
(e: 'insert', code: string): void;
|
(e: 'insert', code: string): void;
|
||||||
(e: 'formApiImport'): void; // 从api 定义导入
|
(e: 'formApiImport'): void; // 从api 定义导入
|
||||||
(e: 'insertCommonScript'): void; // 从api 定义导入
|
(e: 'insertCommonScript'): void; // 从api 定义导入
|
||||||
(e: 'updateLanguages', value: Languages): void; // 从api 定义导入
|
(e: 'updateLanguages', value: Language): void; // 从api 定义导入
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const innerExpand = useVModel(props, 'expand', emit);
|
const innerExpand = useVModel(props, 'expand', emit);
|
||||||
@ -144,7 +146,7 @@
|
|||||||
function changeHandler(
|
function changeHandler(
|
||||||
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
|
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
|
||||||
) {
|
) {
|
||||||
innerLanguageType.value = value as Languages;
|
innerLanguageType.value = value as Language;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -88,14 +88,12 @@
|
|||||||
import type { CommonScriptItem } from '@/models/projectManagement/commonScript';
|
import type { CommonScriptItem } from '@/models/projectManagement/commonScript';
|
||||||
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
import { RequestConditionScriptLanguage } from '@/enums/apiEnum';
|
||||||
|
|
||||||
import { type Languages } from './utils';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showType: 'commonScript' | 'executionResult'; // 执行类型
|
showType: 'commonScript' | 'executionResult'; // 执行类型
|
||||||
language: Languages | RequestConditionScriptLanguage;
|
language: Language | RequestConditionScriptLanguage;
|
||||||
code: string;
|
code: string;
|
||||||
enableRadioSelected?: boolean;
|
enableRadioSelected?: boolean;
|
||||||
executionResult?: string; // 执行结果
|
executionResult?: string; // 执行结果
|
||||||
@ -106,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:language', value: Languages | RequestConditionScriptLanguage): void;
|
(e: 'update:language', value: Language | RequestConditionScriptLanguage): void;
|
||||||
(e: 'update:code', value: string): void;
|
(e: 'update:code', value: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -157,22 +157,23 @@
|
|||||||
async function testApi() {
|
async function testApi() {
|
||||||
try {
|
try {
|
||||||
testApiLoading.value = true;
|
testApiLoading.value = true;
|
||||||
if (apiConfig.value.id) {
|
|
||||||
// 已经存在配置
|
|
||||||
await updateLocalConfig({
|
|
||||||
id: apiConfig.value.id,
|
|
||||||
userUrl: apiConfig.value.userUrl.trim(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const result = await addLocalConfig({
|
|
||||||
type: 'API',
|
|
||||||
userUrl: apiConfig.value.userUrl.trim(),
|
|
||||||
});
|
|
||||||
apiConfig.value.id = result.id;
|
|
||||||
}
|
|
||||||
const res = await validLocalConfig(apiConfig.value.id);
|
const res = await validLocalConfig(apiConfig.value.id);
|
||||||
apiConfig.value.status = res ? 1 : 2;
|
apiConfig.value.status = res ? 1 : 2;
|
||||||
if (res) {
|
if (res) {
|
||||||
|
// 检测通过才保存配置
|
||||||
|
if (apiConfig.value.id) {
|
||||||
|
// 已经存在配置
|
||||||
|
await updateLocalConfig({
|
||||||
|
id: apiConfig.value.id,
|
||||||
|
userUrl: apiConfig.value.userUrl.trim(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const result = await addLocalConfig({
|
||||||
|
type: 'API',
|
||||||
|
userUrl: apiConfig.value.userUrl.trim(),
|
||||||
|
});
|
||||||
|
apiConfig.value.id = result.id;
|
||||||
|
}
|
||||||
Message.success(t('ms.personal.testPass'));
|
Message.success(t('ms.personal.testPass'));
|
||||||
} else {
|
} else {
|
||||||
Message.error(t('ms.personal.testFail'));
|
Message.error(t('ms.personal.testFail'));
|
||||||
|
@ -35,7 +35,11 @@
|
|||||||
<slot name="extra" v-bind="_props"></slot>
|
<slot name="extra" v-bind="_props"></slot>
|
||||||
<MsTableMoreAction
|
<MsTableMoreAction
|
||||||
v-if="props.nodeMoreActions"
|
v-if="props.nodeMoreActions"
|
||||||
:list="props.nodeMoreActions"
|
:list="
|
||||||
|
typeof props.filterMoreActionFunc === 'function'
|
||||||
|
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
|
||||||
|
: props.nodeMoreActions
|
||||||
|
"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@select="handleNodeMoreSelect($event, _props)"
|
@select="handleNodeMoreSelect($event, _props)"
|
||||||
@close="moreActionsClose"
|
@close="moreActionsClose"
|
||||||
@ -112,6 +116,7 @@
|
|||||||
| 'right'
|
| 'right'
|
||||||
| 'rt'
|
| 'rt'
|
||||||
| 'rb'; // 标题 tooltip 的位置
|
| 'rb'; // 标题 tooltip 的位置
|
||||||
|
filterMoreActionFunc?: (items: ActionsItem[], node: MsTreeNodeData) => ActionsItem[]; // 过滤更多操作按钮
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
searchDebounce: 300,
|
searchDebounce: 300,
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
v-for="tab in props.tabs"
|
v-for="tab in props.tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
class="ms-editable-tab"
|
class="ms-editable-tab"
|
||||||
:class="{ active: innerActiveTab === tab.id }"
|
:class="{ active: innerActiveTab?.id === tab.id }"
|
||||||
@click="handleTabClick(tab)"
|
@click="handleTabClick(tab)"
|
||||||
>
|
>
|
||||||
<div :draggable="!!tab.draggable" class="flex items-center">
|
<div :draggable="!!tab.draggable" class="flex items-center">
|
||||||
@ -46,6 +46,7 @@
|
|||||||
</MsButton>
|
</MsButton>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
|
v-if="!props.readonly"
|
||||||
:content="t('ms.editableTab.limitTip', { max: props.limit })"
|
:content="t('ms.editableTab.limitTip', { max: props.limit })"
|
||||||
:disabled="!props.limit || props.tabs.length >= props.limit"
|
:disabled="!props.limit || props.tabs.length >= props.limit"
|
||||||
>
|
>
|
||||||
@ -60,9 +61,9 @@
|
|||||||
</MsButton>
|
</MsButton>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<MsMoreAction
|
<MsMoreAction
|
||||||
v-if="props.moreActionList"
|
v-if="!props.hideMoreAction && !props.readonly"
|
||||||
:list="props.moreActionList"
|
:list="mergedMoreActionList"
|
||||||
@select="(val) => emit('moreActionSelect', val)"
|
@select="handleMoreActionSelect"
|
||||||
>
|
>
|
||||||
<MsButton type="icon" status="secondary" class="ms-editable-tab-button">
|
<MsButton type="icon" status="secondary" class="ms-editable-tab-button">
|
||||||
<MsIcon type="icon-icon_more_outlined" />
|
<MsIcon type="icon-icon_more_outlined" />
|
||||||
@ -82,19 +83,22 @@
|
|||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
|
||||||
import type { TabItem } from './types';
|
import type { TabItem } from './types';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tabs: TabItem[];
|
tabs: TabItem[];
|
||||||
activeTab: string | number;
|
activeTab?: TabItem;
|
||||||
moreActionList?: ActionsItem[];
|
moreActionList?: ActionsItem[];
|
||||||
limit?: number; // 最多可打开的tab数量
|
limit?: number; // 最多可打开的tab数量
|
||||||
atLeastOne?: boolean; // 是否至少保留一个tab
|
atLeastOne?: boolean; // 是否至少保留一个tab
|
||||||
|
hideMoreAction?: boolean; // 是否隐藏更多操作
|
||||||
|
readonly?: boolean; // 是否只读
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:tabs', activeTab: string | number): void;
|
(e: 'update:tabs', tabs: TabItem[]): void;
|
||||||
(e: 'update:activeTab', activeTab: string | number): void;
|
(e: 'update:activeTab', activeTab: TabItem): void;
|
||||||
(e: 'add'): void;
|
(e: 'add'): void;
|
||||||
(e: 'close', item: TabItem): void;
|
(e: 'close', item: TabItem): void;
|
||||||
(e: 'change', item: TabItem): void;
|
(e: 'change', item: TabItem): void;
|
||||||
@ -102,10 +106,11 @@
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
const innerActiveTab = useVModel(props, 'activeTab', emit);
|
||||||
const innerTabs = useVModel(props, 'tabs', emit);
|
const innerTabs = useVModel(props, 'tabs', emit);
|
||||||
const tabNav = ref<HTMLElement | null>(null);
|
const tabNav = ref<HTMLElement>();
|
||||||
const { arrivedState } = useScroll(tabNav);
|
const { arrivedState } = useScroll(tabNav);
|
||||||
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
const isNotOverflow = computed(() => arrivedState.left && arrivedState.right); // 内容是否溢出,用于判断左右滑动按钮是否展示
|
||||||
|
|
||||||
@ -129,7 +134,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const scrollToActiveTab = () => {
|
const scrollToActiveTab = () => {
|
||||||
const activeTabDom = tabNav.value?.querySelector('.tab.active');
|
const activeTabDom = tabNav.value?.querySelector('.ms-editable-tab.active');
|
||||||
if (activeTabDom) {
|
if (activeTabDom) {
|
||||||
const tabRect = activeTabDom.getBoundingClientRect();
|
const tabRect = activeTabDom.getBoundingClientRect();
|
||||||
const navRect = tabNav.value?.getBoundingClientRect();
|
const navRect = tabNav.value?.getBoundingClientRect();
|
||||||
@ -141,22 +146,35 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defualtMoreActionList = [
|
||||||
|
{
|
||||||
|
eventTag: 'closeAll',
|
||||||
|
label: t('ms.editableTab.closeAll'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventTag: 'closeOther',
|
||||||
|
label: t('ms.editableTab.closeOther'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mergedMoreActionList = computed(() => {
|
||||||
|
const dl = props.atLeastOne
|
||||||
|
? defualtMoreActionList.filter((e) => e.eventTag !== 'closeAll')
|
||||||
|
: defualtMoreActionList;
|
||||||
|
return props.moreActionList ? [...dl, ...props.moreActionList] : dl;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.activeTab,
|
() => props.activeTab,
|
||||||
(val) => {
|
() => {
|
||||||
emit('change', props.tabs.find((item) => item.id === val) as TabItem);
|
useDraggable('.ms-editable-tab-nav', innerTabs, {
|
||||||
|
ghostClass: 'ms-editable-tab-ghost',
|
||||||
|
});
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToActiveTab();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(props.tabs, () => {
|
|
||||||
useDraggable('.ms-editable-tab-nav', innerTabs, {
|
|
||||||
ghostClass: 'ms-editable-tab-ghost',
|
|
||||||
});
|
|
||||||
nextTick(() => {
|
|
||||||
scrollToActiveTab();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
scrollToActiveTab();
|
scrollToActiveTab();
|
||||||
@ -168,16 +186,75 @@
|
|||||||
emit('add');
|
emit('add');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeOneTab(item: TabItem) {
|
||||||
|
const index = innerTabs.value.findIndex((e) => e.id === item.id);
|
||||||
|
innerTabs.value.splice(index, 1);
|
||||||
|
if (innerActiveTab.value?.id === item.id && innerTabs.value[0]) {
|
||||||
|
[innerActiveTab.value] = innerTabs.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function close(item: TabItem) {
|
function close(item: TabItem) {
|
||||||
emit('close', item);
|
if (item.unSaved) {
|
||||||
|
openModal({
|
||||||
|
title: t('common.tip'),
|
||||||
|
content: t('ms.editableTab.closeTabTip'),
|
||||||
|
type: 'warning',
|
||||||
|
hideCancel: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
closeOneTab(item);
|
||||||
|
emit('close', item);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
closeOneTab(item);
|
||||||
|
emit('close', item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabClick(item: TabItem) {
|
function handleTabClick(item: TabItem) {
|
||||||
emit('change', item);
|
innerActiveTab.value = item;
|
||||||
innerActiveTab.value = item.id;
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
tabNav.value?.querySelector('.tab.active')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
});
|
});
|
||||||
|
emit('change', item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAction(event: ActionsItem) {
|
||||||
|
switch (event.eventTag) {
|
||||||
|
case 'closeAll':
|
||||||
|
innerTabs.value = innerTabs.value.filter((item) => item.closable === false);
|
||||||
|
[innerActiveTab.value] = innerTabs.value;
|
||||||
|
break;
|
||||||
|
case 'closeOther':
|
||||||
|
innerTabs.value = innerTabs.value.filter(
|
||||||
|
(item) => item.id === innerActiveTab.value?.id || item.closable === false
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit('moreActionSelect', event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoreActionSelect(event: ActionsItem) {
|
||||||
|
if (
|
||||||
|
(event.eventTag === 'closeAll' && innerTabs.value.some((item) => item.unSaved)) ||
|
||||||
|
(event.eventTag === 'closeOther' &&
|
||||||
|
innerTabs.value.some((item) => item.unSaved && item.id !== innerActiveTab.value?.id))
|
||||||
|
) {
|
||||||
|
openModal({
|
||||||
|
title: t('common.tip'),
|
||||||
|
content: t('ms.editableTab.batchCloseTabTip'),
|
||||||
|
type: 'warning',
|
||||||
|
hideCancel: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
executeAction(event);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executeAction(event);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,4 +2,10 @@ export default {
|
|||||||
'ms.editableTab.arrivedLeft': 'Already reached the far left~',
|
'ms.editableTab.arrivedLeft': 'Already reached the far left~',
|
||||||
'ms.editableTab.arrivedRight': 'Already reached the far right~',
|
'ms.editableTab.arrivedRight': 'Already reached the far right~',
|
||||||
'ms.editableTab.limitTip': 'Up to {max} tabs can currently be open',
|
'ms.editableTab.limitTip': 'Up to {max} tabs can currently be open',
|
||||||
|
'ms.editableTab.closeTabTip':
|
||||||
|
'The modified content of this tab has not been saved. The unsaved content will be lost after closing. Are you sure you want to close?',
|
||||||
|
'ms.editableTab.batchCloseTabTip':
|
||||||
|
'The content of some tabs has not been saved. The unsaved content will be lost after closing. Are you sure you want to close?',
|
||||||
|
'ms.editableTab.closeAll': 'Close all',
|
||||||
|
'ms.editableTab.closeOther': 'Close other',
|
||||||
};
|
};
|
||||||
|
@ -2,4 +2,8 @@ export default {
|
|||||||
'ms.editableTab.arrivedLeft': '到最左侧啦~',
|
'ms.editableTab.arrivedLeft': '到最左侧啦~',
|
||||||
'ms.editableTab.arrivedRight': '到最右侧啦~',
|
'ms.editableTab.arrivedRight': '到最右侧啦~',
|
||||||
'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页',
|
'ms.editableTab.limitTip': '当前最多可打开 {max} 个标签页',
|
||||||
|
'ms.editableTab.closeTabTip': '该标签页有改动的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
|
||||||
|
'ms.editableTab.batchCloseTabTip': '有标签页的内容未保存,关闭后未保存的内容将丢失,确定要关闭吗?',
|
||||||
|
'ms.editableTab.closeAll': '关闭全部',
|
||||||
|
'ms.editableTab.closeOther': '关闭其他',
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { type Languages } from '@/components/business/ms-common-script/utils';
|
import { Language } from '@/components/pure/ms-code-editor/types';
|
||||||
|
|
||||||
export interface CommonScriptMenu {
|
export interface CommonScriptMenu {
|
||||||
title: string;
|
title: string;
|
||||||
@ -13,7 +13,7 @@ export interface CommonScriptItem {
|
|||||||
name: string;
|
name: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description: string;
|
description: string;
|
||||||
type: Languages; // 脚本语言类型
|
type: Language; // 脚本语言类型
|
||||||
status: string; // 脚本状态(进行中/已完成)
|
status: string; // 脚本状态(进行中/已完成)
|
||||||
createTime: number;
|
createTime: number;
|
||||||
updateTime: number;
|
updateTime: number;
|
||||||
@ -29,7 +29,7 @@ export interface AddOrUpdateCommonScript {
|
|||||||
id?: string;
|
id?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: Languages;
|
type: Language;
|
||||||
status: string;
|
status: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -226,16 +226,16 @@ export function filterTree<T>(
|
|||||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||||
filterFn: (node: TreeNode<T>) => boolean,
|
filterFn: (node: TreeNode<T>) => boolean,
|
||||||
customChildrenKey = 'children'
|
customChildrenKey = 'children'
|
||||||
): TreeNode<T>[] {
|
): T[] {
|
||||||
if (!Array.isArray(tree)) {
|
if (!Array.isArray(tree)) {
|
||||||
tree = [tree];
|
tree = [tree];
|
||||||
}
|
}
|
||||||
const filteredTree: TreeNode<T>[] = [];
|
const filteredTree: T[] = [];
|
||||||
for (let i = 0; i < tree.length; i++) {
|
for (let i = 0; i < tree.length; i++) {
|
||||||
const node = tree[i];
|
const node = tree[i];
|
||||||
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
|
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
|
||||||
if (filterFn(node)) {
|
if (filterFn(node)) {
|
||||||
const newNode: TreeNode<T> = { ...node };
|
const newNode: T = { ...node };
|
||||||
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
|
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
|
||||||
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
|
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
|
||||||
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);
|
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<div
|
<div
|
||||||
v-if="!condition.enableCommonScript"
|
v-if="!condition.enableCommonScript"
|
||||||
class="relative rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
|
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
|
||||||
>
|
>
|
||||||
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
|
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
|
||||||
<a-input
|
<a-input
|
||||||
@ -84,13 +84,15 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MsScriptDefined
|
<div class="h-[calc(100%-24px)] min-h-[300px]">
|
||||||
v-if="condition.script !== undefined && condition.scriptLanguage !== undefined"
|
<MsScriptDefined
|
||||||
v-model:code="condition.script"
|
v-if="condition.script !== undefined && condition.scriptLanguage !== undefined"
|
||||||
v-model:language="condition.scriptLanguage"
|
v-model:code="condition.script"
|
||||||
show-type="commonScript"
|
v-model:language="condition.scriptLanguage"
|
||||||
:show-header="false"
|
show-type="commonScript"
|
||||||
></MsScriptDefined>
|
:show-header="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex h-[calc(100%-47px)] flex-col">
|
<div v-else class="flex h-[calc(100%-47px)] flex-col">
|
||||||
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
|
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
|
||||||
@ -443,11 +445,19 @@ org.apache.http.client.method . . . '' at line number 2
|
|||||||
columns,
|
columns,
|
||||||
noDisable: true,
|
noDisable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => condition.value.params,
|
||||||
|
(arr) => {
|
||||||
|
propsRes.value.data = arr as any[]; // 查看详情的时候需要赋值一下
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showQuoteDrawer = ref(false);
|
const showQuoteDrawer = ref(false);
|
||||||
function saveQuoteScriptHandler(item: any) {
|
function saveQuoteScriptHandler(item: any) {
|
||||||
condition.value.script = item.script;
|
condition.value.script = item.script;
|
||||||
condition.value.scriptId = item.id;
|
condition.value.scriptId = item.id;
|
||||||
condition.value.scriptName = item.name;
|
condition.value.scriptName = item.name; // TODO:详情接口未返回该字段
|
||||||
condition.value.params = (JSON.parse(item.params) || []).map((e: any) => {
|
condition.value.params = (JSON.parse(item.params) || []).map((e: any) => {
|
||||||
return {
|
return {
|
||||||
key: e.name,
|
key: e.name,
|
||||||
@ -690,7 +700,7 @@ org.apache.http.client.method . . . '' at line number 2
|
|||||||
background-color: var(--color-text-n9);
|
background-color: var(--color-text-n9);
|
||||||
}
|
}
|
||||||
.condition-content {
|
.condition-content {
|
||||||
@apply flex-1 overflow-y-auto;
|
@apply flex flex-1 flex-col overflow-y-auto;
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -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>
|
</template>
|
||||||
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
<div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||||
<a-radio-group
|
<a-radio-group
|
||||||
|
v-if="!props.hideLayoutSwicth"
|
||||||
v-model:model-value="innerLayout"
|
v-model:model-value="innerLayout"
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
@ -183,12 +184,19 @@
|
|||||||
console: string;
|
console: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
activeTab: keyof typeof ResponseComposition;
|
defineProps<{
|
||||||
activeLayout: Direction;
|
activeTab: keyof typeof ResponseComposition;
|
||||||
isExpanded: boolean;
|
activeLayout?: Direction;
|
||||||
response: Response;
|
isExpanded: boolean;
|
||||||
}>();
|
response: Response;
|
||||||
|
hideLayoutSwicth?: boolean; // 隐藏布局切换
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
activeLayout: 'vertical',
|
||||||
|
hideLayoutSwicth: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:activeLayout', value: Direction): void;
|
(e: 'update:activeLayout', value: Direction): void;
|
||||||
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
|
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
|
||||||
@ -285,7 +293,7 @@
|
|||||||
// {
|
// {
|
||||||
// label: t('apiTestDebug.assertion'),
|
// label: t('apiTestDebug.assertion'),
|
||||||
// value: ResponseComposition.ASSERTION,
|
// value: ResponseComposition.ASSERTION,
|
||||||
// },
|
// }, // TODO:断言暂时没加
|
||||||
];
|
];
|
||||||
|
|
||||||
const { copy, isSupported } = useClipboard();
|
const { copy, isSupported } = useClipboard();
|
||||||
@ -310,7 +318,7 @@
|
|||||||
// case ResponseComposition.EXTRACT:
|
// case ResponseComposition.EXTRACT:
|
||||||
// return Object.keys(props.response.extract)
|
// return Object.keys(props.response.extract)
|
||||||
// .map((e) => `${e}: ${props.response.extract[e]}`)
|
// .map((e) => `${e}: ${props.response.extract[e]}`)
|
||||||
// .join('\n');
|
// .join('\n'); // TODO:断言暂时没加
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
@ -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
|
<moduleTree
|
||||||
ref="moduleTreeRef"
|
ref="moduleTreeRef"
|
||||||
@init="(val) => (folderTree = val)"
|
@init="(val) => (folderTree = val)"
|
||||||
@new-api="newApi"
|
@new-api="addDebugTab"
|
||||||
@click-api-node="handleApiNodeClick"
|
@click-api-node="openApiTab"
|
||||||
@import="importDrawerVisible = true"
|
@import="importDrawerVisible = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #second>
|
<template #second>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<debug
|
<div class="border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
|
||||||
ref="debugRef"
|
<MsEditableTab v-model:active-tab="activeDebug" v-model:tabs="debugTabs" at-least-one @add="addDebugTab">
|
||||||
v-model:detail-loading="loading"
|
<template #label="{ tab }">
|
||||||
:module-tree="folderTree"
|
<apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
|
||||||
@add-done="handleDebugAddDone"
|
{{ tab.label }}
|
||||||
/>
|
</template>
|
||||||
|
</MsEditableTab>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<debug
|
||||||
|
v-model:detail-loading="loading"
|
||||||
|
v-model:request="activeDebug"
|
||||||
|
:module-tree="folderTree"
|
||||||
|
:create-api="addDebug"
|
||||||
|
:update-api="updateDebug"
|
||||||
|
:execute-api="executeDebug"
|
||||||
|
@add-done="handleDebugAddDone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MsSplitBox>
|
</MsSplitBox>
|
||||||
@ -58,35 +71,194 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||||
|
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
import debug from './components/debug/index.vue';
|
|
||||||
import moduleTree from './components/moduleTree.vue';
|
import moduleTree from './components/moduleTree.vue';
|
||||||
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
import debug, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
|
||||||
|
import { addDebug, executeDebug, getDebugDetail, updateDebug } from '@/api/modules/api-test/debug';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { parseCurlScript } from '@/utils';
|
import { parseCurlScript } from '@/utils';
|
||||||
|
|
||||||
|
import { ExecuteBody } from '@/models/apiTest/debug';
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
import { RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
|
import {
|
||||||
|
RequestAuthType,
|
||||||
|
RequestBodyFormat,
|
||||||
|
RequestComposition,
|
||||||
|
RequestContentTypeEnum,
|
||||||
|
RequestMethods,
|
||||||
|
RequestParamsType,
|
||||||
|
ResponseComposition,
|
||||||
|
} from '@/enums/apiEnum';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
|
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
|
||||||
const debugRef = ref<InstanceType<typeof debug>>();
|
|
||||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
const importDrawerVisible = ref(false);
|
const importDrawerVisible = ref(false);
|
||||||
const curlCode = ref('');
|
const curlCode = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
function newApi() {
|
function handleDebugAddDone() {
|
||||||
debugRef.value?.addDebugTab();
|
moduleTreeRef.value?.initModules();
|
||||||
|
moduleTreeRef.value?.initModuleCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
const initDefaultId = `debug-${Date.now()}`;
|
||||||
|
const defaultBodyParams: ExecuteBody = {
|
||||||
|
bodyType: RequestBodyFormat.NONE,
|
||||||
|
formDataBody: {
|
||||||
|
formValues: [],
|
||||||
|
},
|
||||||
|
wwwFormBody: {
|
||||||
|
formValues: [],
|
||||||
|
},
|
||||||
|
jsonBody: {
|
||||||
|
jsonValue: '',
|
||||||
|
},
|
||||||
|
xmlBody: { value: '' },
|
||||||
|
binaryBody: {
|
||||||
|
description: '',
|
||||||
|
file: undefined,
|
||||||
|
},
|
||||||
|
rawBody: { value: '' },
|
||||||
|
};
|
||||||
|
const defaultResponse = {
|
||||||
|
requestResults: [
|
||||||
|
{
|
||||||
|
body: '',
|
||||||
|
responseResult: {
|
||||||
|
body: '',
|
||||||
|
contentType: '',
|
||||||
|
headers: '',
|
||||||
|
dnsLookupTime: 0,
|
||||||
|
downloadTime: 0,
|
||||||
|
latency: 0,
|
||||||
|
responseCode: 0,
|
||||||
|
responseTime: 0,
|
||||||
|
responseSize: 0,
|
||||||
|
socketInitTime: 0,
|
||||||
|
tcpHandshakeTime: 0,
|
||||||
|
transferStartTime: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
console: '',
|
||||||
|
}; // 调试返回的响应内容
|
||||||
|
const defaultDebugParams: RequestParam = {
|
||||||
|
id: initDefaultId,
|
||||||
|
moduleId: 'root',
|
||||||
|
protocol: 'HTTP',
|
||||||
|
url: '',
|
||||||
|
activeTab: RequestComposition.HEADER,
|
||||||
|
label: t('apiTestDebug.newApi'),
|
||||||
|
closable: true,
|
||||||
|
method: RequestMethods.GET,
|
||||||
|
unSaved: false,
|
||||||
|
headers: [],
|
||||||
|
body: cloneDeep(defaultBodyParams),
|
||||||
|
query: [],
|
||||||
|
rest: [],
|
||||||
|
polymorphicName: '',
|
||||||
|
name: '',
|
||||||
|
path: '',
|
||||||
|
projectId: '',
|
||||||
|
uploadFileIds: [],
|
||||||
|
linkFileIds: [],
|
||||||
|
authConfig: {
|
||||||
|
authType: RequestAuthType.NONE,
|
||||||
|
userName: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||||
|
assertionConfig: {
|
||||||
|
enableGlobal: false,
|
||||||
|
assertions: [],
|
||||||
|
},
|
||||||
|
postProcessorConfig: {
|
||||||
|
enableGlobal: false,
|
||||||
|
processors: [],
|
||||||
|
},
|
||||||
|
preProcessorConfig: {
|
||||||
|
enableGlobal: false,
|
||||||
|
processors: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
otherConfig: {
|
||||||
|
connectTimeout: 60000,
|
||||||
|
responseTimeout: 60000,
|
||||||
|
certificateAlias: '',
|
||||||
|
followRedirects: true,
|
||||||
|
autoRedirects: false,
|
||||||
|
},
|
||||||
|
responseActiveTab: ResponseComposition.BODY,
|
||||||
|
response: cloneDeep(defaultResponse),
|
||||||
|
};
|
||||||
|
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
|
||||||
|
const activeDebug = ref<RequestParam>(debugTabs.value[0]);
|
||||||
|
const isHttpProtocol = computed(() => activeDebug.value.protocol === 'HTTP');
|
||||||
|
|
||||||
|
function handleActiveDebugChange() {
|
||||||
|
if (!loading.value) {
|
||||||
|
// 如果是因为加载详情触发的change则不需要标记为未保存
|
||||||
|
activeDebug.value.unSaved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDebugTab(defaultProps?: Partial<TabItem>) {
|
||||||
|
const id = `debug-${Date.now()}`;
|
||||||
|
debugTabs.value.push({
|
||||||
|
...cloneDeep(defaultDebugParams),
|
||||||
|
id,
|
||||||
|
isNew: !defaultProps?.id, // 新开的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() {
|
function handleCurlImportConfirm() {
|
||||||
const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
|
const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
|
||||||
debugRef.value?.addDebugTab({
|
addDebugTab({
|
||||||
url,
|
url,
|
||||||
headers: headers?.map((e) => ({
|
headers: headers?.map((e) => ({
|
||||||
contentType: RequestContentTypeEnum.TEXT,
|
contentType: RequestContentTypeEnum.TEXT,
|
||||||
@ -107,15 +279,9 @@
|
|||||||
});
|
});
|
||||||
curlCode.value = '';
|
curlCode.value = '';
|
||||||
importDrawerVisible.value = false;
|
importDrawerVisible.value = false;
|
||||||
}
|
nextTick(() => {
|
||||||
|
handleActiveDebugChange();
|
||||||
function handleApiNodeClick(node: ModuleTreeNode) {
|
});
|
||||||
debugRef.value?.openApiTab(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDebugAddDone() {
|
|
||||||
moduleTreeRef.value?.initModules();
|
|
||||||
moduleTreeRef.value?.initModuleCount();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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>
|
<template>
|
||||||
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
|
<div :class="['p-[16px_22px]', props.class]">
|
||||||
<MsEditableTab
|
<div class="mb-[16px] flex items-center justify-between">
|
||||||
v-model:active-tab="activeRequestTab"
|
|
||||||
v-model:tabs="apiTabs"
|
|
||||||
:more-action-list="tabMoreActionList"
|
|
||||||
@add="addDebugTab"
|
|
||||||
@close="closeDebugTab"
|
|
||||||
@change="setActiveDebug"
|
|
||||||
@more-action-select="handleTabMoreActionSelect"
|
|
||||||
>
|
|
||||||
<template #label="{ tab }">
|
|
||||||
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" />
|
|
||||||
{{ tab.label }}
|
|
||||||
</template>
|
|
||||||
</MsEditableTab>
|
|
||||||
</div>
|
|
||||||
<div class="p-[16px_22px]">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-[8px]">
|
<div class="flex items-center gap-[8px]">
|
||||||
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
|
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
|
||||||
{{ t('apiTestManagement.showSubdirectory') }}
|
{{ t('apiTestManagement.showSubdirectory') }}
|
||||||
@ -31,7 +15,7 @@
|
|||||||
v-model:model-value="checkedEnv"
|
v-model:model-value="checkedEnv"
|
||||||
mode="static"
|
mode="static"
|
||||||
:options="envOptions"
|
:options="envOptions"
|
||||||
class="w-[200px]"
|
class="!w-[150px]"
|
||||||
:search-keys="['label']"
|
:search-keys="['label']"
|
||||||
allow-search
|
allow-search
|
||||||
/>
|
/>
|
||||||
@ -53,6 +37,7 @@
|
|||||||
<ms-base-table
|
<ms-base-table
|
||||||
v-bind="propsRes"
|
v-bind="propsRes"
|
||||||
:action-config="batchActions"
|
:action-config="batchActions"
|
||||||
|
:first-column-width="44"
|
||||||
no-disable
|
no-disable
|
||||||
filter-icon-align-left
|
filter-icon-align-left
|
||||||
v-on="propsEvent"
|
v-on="propsEvent"
|
||||||
@ -251,8 +236,6 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
|
||||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
@ -260,10 +243,10 @@
|
|||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
import MsSelect from '@/components/business/ms-select';
|
||||||
import moduleTree from '../moduleTree.vue';
|
|
||||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
|
import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
@ -274,79 +257,22 @@
|
|||||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
module: string;
|
class?: string;
|
||||||
allCount: number;
|
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
offspringIds: string[];
|
offspringIds: string[];
|
||||||
|
readOnly?: boolean; // 是否是只读模式
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'init', params: any): void;
|
(e: 'init', params: any): void;
|
||||||
|
(e: 'change'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const activeRequestTab = ref<string | number>('all');
|
|
||||||
const apiTabs = ref<TabItem[]>([
|
|
||||||
{
|
|
||||||
id: 'all',
|
|
||||||
label: `${t('apiTestManagement.allApi')}(${props.allCount})`,
|
|
||||||
closable: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const activeApiTab = ref<TabItem>(apiTabs.value[0]);
|
|
||||||
|
|
||||||
function setActiveDebug(item: TabItem) {
|
|
||||||
activeApiTab.value = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleActiveDebugChange() {
|
function handleActiveDebugChange() {
|
||||||
activeApiTab.value.unSaved = true;
|
emit('change');
|
||||||
}
|
|
||||||
|
|
||||||
function addDebugTab(defaultProps?: Partial<TabItem>) {
|
|
||||||
const id = `debug-${Date.now()}`;
|
|
||||||
apiTabs.value.push({
|
|
||||||
module: props.module,
|
|
||||||
label: t('apiTestDebug.newApi'),
|
|
||||||
id,
|
|
||||||
...defaultProps,
|
|
||||||
});
|
|
||||||
activeRequestTab.value = id;
|
|
||||||
nextTick(() => {
|
|
||||||
if (defaultProps) {
|
|
||||||
handleActiveDebugChange();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDebugTab(tab: TabItem) {
|
|
||||||
const index = apiTabs.value.findIndex((item) => item.id === tab.id);
|
|
||||||
apiTabs.value.splice(index, 1);
|
|
||||||
if (activeRequestTab.value === tab.id) {
|
|
||||||
activeRequestTab.value = apiTabs.value[0]?.id || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabMoreActionList = [
|
|
||||||
{
|
|
||||||
eventTag: 'closeAll',
|
|
||||||
label: t('apiTestManagement.closeAll'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventTag: 'closeOther',
|
|
||||||
label: t('apiTestManagement.closeOther'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function handleTabMoreActionSelect(event: ActionsItem) {
|
|
||||||
if (event.eventTag === 'closeOther') {
|
|
||||||
apiTabs.value = apiTabs.value.filter((item) => item.id === activeRequestTab.value || item.closable === false);
|
|
||||||
} else if (event.eventTag === 'closeAll') {
|
|
||||||
apiTabs.value = apiTabs.value.filter((item) => item.id === 'all');
|
|
||||||
activeRequestTab.value = 'all';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSubdirectory = ref(false);
|
const showSubdirectory = ref(false);
|
||||||
@ -371,7 +297,7 @@
|
|||||||
]);
|
]);
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
|
|
||||||
const columns: MsTableColumn = [
|
let columns: MsTableColumn = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'num',
|
dataIndex: 'num',
|
||||||
@ -455,32 +381,38 @@
|
|||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableStore = useTableStore();
|
if (!props.readOnly) {
|
||||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
const tableStore = useTableStore();
|
||||||
|
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
||||||
|
} else {
|
||||||
|
columns = columns.filter(
|
||||||
|
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
||||||
|
);
|
||||||
|
}
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
() =>
|
() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
id: 1001,
|
num: 1001,
|
||||||
name: 'asdasdasd',
|
name: 'asdasdasd',
|
||||||
type: RequestMethods.CONNECT,
|
type: RequestMethods.CONNECT,
|
||||||
status: RequestDefinitionStatus.DEBUGGING,
|
status: RequestDefinitionStatus.DEBUGGING,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10011,
|
num: 10011,
|
||||||
name: '1123',
|
name: '1123',
|
||||||
type: RequestMethods.OPTIONS,
|
type: RequestMethods.OPTIONS,
|
||||||
status: RequestDefinitionStatus.DEPRECATED,
|
status: RequestDefinitionStatus.DEPRECATED,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10012,
|
num: 10012,
|
||||||
name: 'vfd',
|
name: 'vfd',
|
||||||
type: RequestMethods.POST,
|
type: RequestMethods.POST,
|
||||||
status: RequestDefinitionStatus.DONE,
|
status: RequestDefinitionStatus.DONE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10013,
|
num: 10013,
|
||||||
name: 'ccf',
|
name: 'ccf',
|
||||||
type: RequestMethods.DELETE,
|
type: RequestMethods.DELETE,
|
||||||
status: RequestDefinitionStatus.PROCESSING,
|
status: RequestDefinitionStatus.PROCESSING,
|
||||||
@ -489,11 +421,13 @@
|
|||||||
total: 0,
|
total: 0,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
tableKey: TableKeyEnum.API_TEST,
|
columns: props.readOnly ? columns : [],
|
||||||
showSetting: true,
|
scroll: { x: '100%' },
|
||||||
|
tableKey: props.readOnly ? undefined : TableKeyEnum.API_TEST,
|
||||||
|
showSetting: !props.readOnly,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
showSelectAll: true,
|
showSelectAll: !props.readOnly,
|
||||||
draggable: { type: 'handle', width: 32 },
|
draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
|
||||||
},
|
},
|
||||||
(item) => ({
|
(item) => ({
|
||||||
...item,
|
...item,
|
||||||
@ -720,6 +654,7 @@
|
|||||||
resetSelector();
|
resetSelector();
|
||||||
loadList();
|
loadList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
batchUpdateLoading.value = false;
|
batchUpdateLoading.value = false;
|
@ -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>
|
<template>
|
||||||
<a-tabs v-model:active-key="activeTab" animation lazy-load>
|
<a-tabs v-model:active-key="activeTab" animation lazy-load class="ms-api-tab-nav">
|
||||||
<a-tab-pane key="api" title="API">
|
<a-tab-pane key="api" title="API" class="ms-api-tab-pane">
|
||||||
<api
|
<api
|
||||||
:active-module="activeModule"
|
ref="apiRef"
|
||||||
|
:module-tree="props.moduleTree"
|
||||||
|
:active-module="props.activeModule"
|
||||||
:module="props.module"
|
:module="props.module"
|
||||||
:all-count="props.allCount"
|
:all-count="props.allCount"
|
||||||
:offspring-ids="props.offspringIds"
|
:offspring-ids="props.offspringIds"
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="case" title="CASE"> </a-tab-pane>
|
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
|
||||||
<a-tab-pane key="mock" title="MOCK"> </a-tab-pane>
|
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
|
||||||
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')"> </a-tab-pane>
|
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')" class="ms-api-tab-pane"> </a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from './api.vue';
|
import api from './api/index.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
module: string;
|
module: string;
|
||||||
allCount: number;
|
allCount: number;
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
offspringIds: string[];
|
offspringIds: string[];
|
||||||
|
moduleTree: ModuleTreeNode[]; // 模块树
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const activeTab = ref('api');
|
const activeTab = ref('api');
|
||||||
|
const apiRef = ref<InstanceType<typeof api>>();
|
||||||
|
|
||||||
|
function newTab(apiInfo?: ModuleTreeNode) {
|
||||||
|
if (apiInfo) {
|
||||||
|
apiRef.value?.openApiTab(apiInfo);
|
||||||
|
} else {
|
||||||
|
apiRef.value?.addApiTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
newTab,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.arco-tabs-nav) {
|
.ms-api-tab-nav {
|
||||||
border-bottom: 1px solid var(--color-text-n8);
|
@apply h-full;
|
||||||
|
:deep(.arco-tabs-content) {
|
||||||
|
height: calc(100% - 51px);
|
||||||
|
.arco-tabs-content-list {
|
||||||
|
@apply h-full;
|
||||||
|
.arco-tabs-pane {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-tabs-nav) {
|
||||||
|
border-bottom: 1px solid var(--color-text-n8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="!props.isModal">
|
<a-select
|
||||||
<a-select
|
v-model:model-value="moduleProtocol"
|
||||||
v-model:model-value="moduleProtocol"
|
:options="moduleProtocolOptions"
|
||||||
:options="moduleProtocolOptions"
|
class="mb-[8px]"
|
||||||
class="mb-[8px]"
|
@change="(val) => handleProtocolChange(val as string)"
|
||||||
@change="(val) => handleProtocolChange(val as string)"
|
/>
|
||||||
/>
|
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||||
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
<a-dropdown v-if="!props.readOnly" @select="handleSelect">
|
||||||
<a-dropdown @select="handleSelect">
|
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||||
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
<template #content>
|
||||||
<template #content>
|
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||||
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
||||||
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
</template>
|
||||||
</template>
|
</a-dropdown>
|
||||||
</a-dropdown>
|
</div>
|
||||||
|
<div class="folder" @click="setActiveFolder('all')">
|
||||||
|
<div :class="allFolderClass">
|
||||||
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
|
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
|
||||||
|
<div class="folder-count">({{ allFileCount }})</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="folder" @click="setActiveFolder('all')">
|
<div class="ml-auto flex items-center">
|
||||||
<div :class="allFolderClass">
|
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
||||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
||||||
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
|
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||||
<div class="folder-count">({{ allFileCount }})</div>
|
</MsButton>
|
||||||
</div>
|
</a-tooltip>
|
||||||
<div class="ml-auto flex items-center">
|
<template v-if="!props.readOnly">
|
||||||
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
|
||||||
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
|
||||||
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
|
||||||
</MsButton>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-dropdown @select="handleSelect">
|
<a-dropdown @select="handleSelect">
|
||||||
<MsButton type="icon" class="!mr-0 p-[2px]">
|
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||||
<MsIcon
|
<MsIcon
|
||||||
@ -45,17 +45,10 @@
|
|||||||
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
|
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
|
||||||
<span id="addModulePopSpan"></span>
|
<span id="addModulePopSpan"></span>
|
||||||
</popConfirm>
|
</popConfirm>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<a-divider class="my-[8px]" />
|
</div>
|
||||||
</template>
|
<a-divider class="my-[8px]" />
|
||||||
<a-input
|
|
||||||
v-if="props.isModal"
|
|
||||||
v-model:model-value="moduleKeyword"
|
|
||||||
:placeholder="t('apiTestManagement.moveSearchTip')"
|
|
||||||
allow-clear
|
|
||||||
class="mb-[16px]"
|
|
||||||
/>
|
|
||||||
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
||||||
<MsTree
|
<MsTree
|
||||||
v-model:focus-node-key="focusNodeKey"
|
v-model:focus-node-key="focusNodeKey"
|
||||||
@ -73,6 +66,7 @@
|
|||||||
children: 'children',
|
children: 'children',
|
||||||
count: 'count',
|
count: 'count',
|
||||||
}"
|
}"
|
||||||
|
:filter-more-action-func="filterMoreActionFunc"
|
||||||
block-node
|
block-node
|
||||||
title-tooltip-position="left"
|
title-tooltip-position="left"
|
||||||
@select="folderNodeSelect"
|
@select="folderNodeSelect"
|
||||||
@ -81,15 +75,23 @@
|
|||||||
@drop="handleDrop"
|
@drop="handleDrop"
|
||||||
>
|
>
|
||||||
<template #title="nodeData">
|
<template #title="nodeData">
|
||||||
<div class="inline-flex w-full">
|
<div
|
||||||
|
v-if="nodeData.type === 'API'"
|
||||||
|
class="inline-flex w-full cursor-pointer gap-[4px]"
|
||||||
|
@click="emit('clickApiNode', nodeData)"
|
||||||
|
>
|
||||||
|
<apiMethodName :method="nodeData.attachInfo?.method || nodeData.attachInfo?.protocol" />
|
||||||
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
<div v-if="!props.isModal" class="ml-auto text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
</div>
|
||||||
|
<div v-else class="inline-flex w-full">
|
||||||
|
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
|
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!props.isModal" #extra="nodeData">
|
<template v-if="!props.readOnly" #extra="nodeData">
|
||||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||||
<popConfirm
|
<popConfirm
|
||||||
v-if="nodeData.id !== 'root'"
|
v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
|
||||||
mode="add"
|
mode="add"
|
||||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||||
:parent-id="nodeData.id"
|
:parent-id="nodeData.id"
|
||||||
@ -120,49 +122,70 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message, SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||||
|
|
||||||
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
|
import {
|
||||||
|
deleteDebugModule,
|
||||||
|
getDebugModuleCount,
|
||||||
|
getDebugModules,
|
||||||
|
moveDebugModule,
|
||||||
|
} from '@/api/modules/api-test/debug';
|
||||||
|
import { getProtocolList } from '@/api/modules/api-test/management';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import { mapTree } from '@/utils';
|
import { filterTree, mapTree } from '@/utils';
|
||||||
|
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
|
||||||
isExpandAll?: boolean; // 是否展开所有节点
|
isExpandAll?: boolean; // 是否展开所有节点
|
||||||
activeModule?: string | number; // 选中的节点 key
|
activeModule?: string | number; // 选中的节点 key
|
||||||
isModal?: boolean; // 是否是弹窗模式
|
readOnly?: boolean; // 是否是只读模式
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
activeModule: 'all',
|
activeModule: 'all',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits(['init', 'change', 'newApi', 'import', 'folderNodeSelect']);
|
const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode']);
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const moduleProtocol = ref('HTTP');
|
const moduleProtocol = ref('HTTP');
|
||||||
const moduleProtocolOptions = ref([
|
const moduleProtocolOptions = ref<SelectOptionData[]>([]);
|
||||||
{
|
const protocolLoading = ref(false);
|
||||||
label: 'HTTP',
|
|
||||||
value: 'HTTP',
|
async function initProtocolList() {
|
||||||
},
|
try {
|
||||||
]);
|
protocolLoading.value = true;
|
||||||
|
const res = await getProtocolList(appStore.currentOrgId);
|
||||||
|
moduleProtocolOptions.value = res.map((e) => ({
|
||||||
|
label: e.protocol,
|
||||||
|
value: e.protocol,
|
||||||
|
polymorphicName: e.polymorphicName,
|
||||||
|
pluginId: e.pluginId,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
protocolLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleProtocolChange(value: string | number | Record<string, any>) {
|
function handleProtocolChange(value: string | number | Record<string, any>) {
|
||||||
|
// TODO:搜索
|
||||||
console.log(value);
|
console.log(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +206,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const virtualListProps = computed(() => {
|
const virtualListProps = computed(() => {
|
||||||
if (props.isModal) {
|
if (props.readOnly) {
|
||||||
return {
|
return {
|
||||||
height: 'calc(60vh - 190px)',
|
height: 'calc(60vh - 190px)',
|
||||||
};
|
};
|
||||||
@ -193,7 +216,6 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const allFileCount = ref(0);
|
|
||||||
const isExpandAll = ref(props.isExpandAll);
|
const isExpandAll = ref(props.isExpandAll);
|
||||||
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
||||||
|
|
||||||
@ -217,13 +239,6 @@
|
|||||||
);
|
);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => selectedKeys.value,
|
|
||||||
(arr) => {
|
|
||||||
emit('change', arr ? arr[0] : '');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function setActiveFolder(id: string) {
|
function setActiveFolder(id: string) {
|
||||||
selectedKeys.value = [id];
|
selectedKeys.value = [id];
|
||||||
}
|
}
|
||||||
@ -244,6 +259,10 @@
|
|||||||
label: 'apiTestManagement.share',
|
label: 'apiTestManagement.share',
|
||||||
eventTag: 'share',
|
eventTag: 'share',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'apiTestManagement.shareModule',
|
||||||
|
eventTag: 'shareModule',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isDivider: true,
|
isDivider: true,
|
||||||
},
|
},
|
||||||
@ -253,8 +272,19 @@
|
|||||||
danger: true,
|
danger: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const renamePopVisible = ref(false);
|
const moduleActions = folderMoreActions.filter(
|
||||||
|
(action) => action.eventTag === undefined || !['execute', 'share'].includes(action.eventTag)
|
||||||
|
);
|
||||||
|
const apiActions = folderMoreActions.filter((action) => action.eventTag !== 'shareModule');
|
||||||
|
function filterMoreActionFunc(actions, node) {
|
||||||
|
if (node.type === 'MODULE') {
|
||||||
|
return moduleActions;
|
||||||
|
}
|
||||||
|
return apiActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modulesCount = ref<Record<string, number>>({});
|
||||||
|
const allFileCount = computed(() => modulesCount.value.all || 0);
|
||||||
/**
|
/**
|
||||||
* 初始化模块树
|
* 初始化模块树
|
||||||
* @param isSetDefaultKey 是否设置第一个节点为选中节点
|
* @param isSetDefaultKey 是否设置第一个节点为选中节点
|
||||||
@ -262,15 +292,29 @@
|
|||||||
async function initModules(isSetDefaultKey = false) {
|
async function initModules(isSetDefaultKey = false) {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await getReviewModules(appStore.currentProjectId);
|
const res = await getDebugModules();
|
||||||
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
if (props.readOnly) {
|
||||||
return {
|
folderTree.value = filterTree<ModuleTreeNode>(res, (e) => {
|
||||||
...e,
|
if (e.type === 'MODULE') {
|
||||||
hideMoreAction: e.id === 'root',
|
e = {
|
||||||
draggable: e.id !== 'root' && !props.isModal,
|
...e,
|
||||||
disabled: e.id === selectedKeys.value[0] && props.isModal,
|
hideMoreAction: true,
|
||||||
};
|
draggable: false,
|
||||||
});
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
hideMoreAction: e.id === 'root',
|
||||||
|
draggable: e.id !== 'root' && !props.readOnly,
|
||||||
|
disabled: e.id === selectedKeys.value[0] && props.readOnly,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
if (isSetDefaultKey) {
|
if (isSetDefaultKey) {
|
||||||
selectedKeys.value = [folderTree.value[0].id];
|
selectedKeys.value = [folderTree.value[0].id];
|
||||||
}
|
}
|
||||||
@ -283,16 +327,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initModuleCount() {
|
||||||
|
try {
|
||||||
|
const res = await getDebugModuleCount({
|
||||||
|
keyword: moduleKeyword.value,
|
||||||
|
});
|
||||||
|
modulesCount.value = res;
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
count: res[node.id] || 0,
|
||||||
|
draggable: node.id !== 'root',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理文件夹树节点选中事件
|
* 处理文件夹树节点选中事件
|
||||||
*/
|
*/
|
||||||
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
||||||
const offspringIds: string[] = [];
|
if (node.type === 'MODULE') {
|
||||||
mapTree(node.children || [], (e) => {
|
const offspringIds: string[] = [];
|
||||||
offspringIds.push(e.id);
|
mapTree(node.children || [], (e) => {
|
||||||
return e;
|
offspringIds.push(e.id);
|
||||||
});
|
return e;
|
||||||
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
});
|
||||||
|
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,9 +376,10 @@
|
|||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
onBeforeOk: async () => {
|
onBeforeOk: async () => {
|
||||||
try {
|
try {
|
||||||
await deleteReviewModule(node.id);
|
await deleteDebugModule(node.id);
|
||||||
Message.success(t('apiTestDebug.deleteSuccess'));
|
Message.success(t('apiTestDebug.deleteSuccess'));
|
||||||
initModules();
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -323,6 +389,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renamePopVisible = ref(false);
|
||||||
const renameFolderTitle = ref(''); // 重命名的文件夹名称
|
const renameFolderTitle = ref(''); // 重命名的文件夹名称
|
||||||
|
|
||||||
function resetFocusNodeKey() {
|
function resetFocusNodeKey() {
|
||||||
@ -366,7 +433,7 @@
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await moveReviewModule({
|
await moveDebugModule({
|
||||||
dragNodeId: dragNode.id as string,
|
dragNodeId: dragNode.id as string,
|
||||||
dropNodeId: dropNode.id || '',
|
dropNodeId: dropNode.id || '',
|
||||||
dropPosition,
|
dropPosition,
|
||||||
@ -377,7 +444,8 @@
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
initModules();
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,27 +456,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(async () => {
|
||||||
initModules();
|
initProtocolList();
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化模块文件数量
|
|
||||||
*/
|
|
||||||
watch(
|
|
||||||
() => props.modulesCount,
|
|
||||||
(obj) => {
|
|
||||||
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
count: obj?.[node.id] || 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
initModules,
|
initModules,
|
||||||
|
initModuleCount,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
<moduleTree
|
<moduleTree
|
||||||
@init="(val) => (folderTree = val)"
|
@init="(val) => (folderTree = val)"
|
||||||
@new-api="newApi"
|
@new-api="newApi"
|
||||||
@change="(val) => (activeModule = val)"
|
|
||||||
@import="importDrawerVisible = true"
|
@import="importDrawerVisible = true"
|
||||||
@folder-node-select="(keys, _offspringIds) => (offspringIds = _offspringIds)"
|
@folder-node-select="handleNodeSelect"
|
||||||
|
@click-api-node="handleApiNodeClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="b-0 absolute w-[88%]">
|
<!-- <div class="b-0 absolute w-[88%]">
|
||||||
@ -36,6 +36,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<management
|
<management
|
||||||
|
ref="managementRef"
|
||||||
|
:module-tree="folderTree"
|
||||||
:module="activeModule"
|
:module="activeModule"
|
||||||
:all-count="allCount"
|
:all-count="allCount"
|
||||||
:active-module="activeModule"
|
:active-module="activeModule"
|
||||||
@ -61,9 +63,19 @@
|
|||||||
const allCount = ref(0);
|
const allCount = ref(0);
|
||||||
const importDrawerVisible = ref(false);
|
const importDrawerVisible = ref(false);
|
||||||
const offspringIds = ref<string[]>([]);
|
const offspringIds = ref<string[]>([]);
|
||||||
|
const managementRef = ref<InstanceType<typeof management>>();
|
||||||
|
|
||||||
function newApi() {
|
function newApi() {
|
||||||
// debugRef.value?.addDebugTab();
|
managementRef.value?.newTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeSelect(keys: string[], _offspringIds: string[]) {
|
||||||
|
[activeModule.value] = keys;
|
||||||
|
offspringIds.value = _offspringIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApiNodeClick(node: ModuleTreeNode) {
|
||||||
|
managementRef.value?.newTab(node);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export default {
|
|||||||
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口',
|
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口',
|
||||||
'apiTestManagement.execute': '执行',
|
'apiTestManagement.execute': '执行',
|
||||||
'apiTestManagement.share': '分享 API',
|
'apiTestManagement.share': '分享 API',
|
||||||
|
'apiTestManagement.shareModule': '分享模块',
|
||||||
'apiTestManagement.doc': '文档',
|
'apiTestManagement.doc': '文档',
|
||||||
'apiTestManagement.closeAll': '关闭全部tab',
|
'apiTestManagement.closeAll': '关闭全部tab',
|
||||||
'apiTestManagement.closeOther': '关闭其他tab',
|
'apiTestManagement.closeOther': '关闭其他tab',
|
||||||
@ -72,4 +73,13 @@ export default {
|
|||||||
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
|
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
|
||||||
'apiTestManagement.timeTaskDay': '(每天)',
|
'apiTestManagement.timeTaskDay': '(每天)',
|
||||||
'apiTestManagement.customFrequency': '自定义频率',
|
'apiTestManagement.customFrequency': '自定义频率',
|
||||||
|
'apiTestManagement.case': '用例',
|
||||||
|
'apiTestManagement.definition': '定义',
|
||||||
|
'apiTestManagement.addDependency': '添加依赖关系',
|
||||||
|
'apiTestManagement.preDependency': '前置依赖',
|
||||||
|
'apiTestManagement.addPreDependency': '添加前置依赖',
|
||||||
|
'apiTestManagement.postDependency': '后置依赖',
|
||||||
|
'apiTestManagement.addPostDependency': '添加后置依赖',
|
||||||
|
'apiTestManagement.saveAsCase': '保存为新用例',
|
||||||
|
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
|
import { Language } from '@/components/pure/ms-code-editor/types';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
|
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
export type Languages = 'groovy' | 'python' | 'beanshell' | 'nashornScript' | 'rhinoScript' | 'javascript';
|
|
||||||
|
|
||||||
export const SCRIPT_MENU: CommonScriptMenu[] = [
|
export const SCRIPT_MENU: CommonScriptMenu[] = [
|
||||||
{
|
{
|
||||||
title: t('project.code_segment.importApiTest'),
|
title: t('project.code_segment.importApiTest'),
|
||||||
@ -505,7 +505,7 @@ function jsCode(requestObj) {
|
|||||||
return _jsTemplate(requestObj);
|
return _jsTemplate(requestObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCodeTemplate(language: Languages, requestObj: any) {
|
export function getCodeTemplate(language: Language, requestObj: any) {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case 'groovy':
|
case 'groovy':
|
||||||
return groovyCode(requestObj);
|
return groovyCode(requestObj);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<PostTab v-model:params="params" layout="horizontal" />
|
<PostTab v-model:config="params" layout="horizontal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import PostTab from '@/views/api-test/debug/components/debug/postcondition.vue';
|
import PostTab from '@/views/api-test/components/requestComposition/postcondition.vue';
|
||||||
|
|
||||||
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
|
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<PreTab v-model:params="params" layout="horizontal" />
|
<PreTab v-model:config="params" layout="horizontal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import PreTab from '@/views/api-test/debug/components/debug/precondition.vue';
|
import PreTab from '@/views/api-test/components/requestComposition/precondition.vue';
|
||||||
|
|
||||||
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
|
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user