feat(接口场景): 场景大框架&接口定义列表执行
This commit is contained in:
parent
f8343ea765
commit
1c86838b8b
|
@ -32,7 +32,12 @@
|
|||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<MsButton type="text" class="more-btn" @click="toggleExpand">
|
||||
<MsButton
|
||||
v-if="props.simpleShowCount !== undefined && props.simpleShowCount > 0"
|
||||
type="text"
|
||||
class="more-btn"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
<div v-if="isExpand" class="flex items-center gap-[4px]">
|
||||
{{ t('msDetailCard.collapse') }}
|
||||
<icon-up :size="14" />
|
||||
|
|
|
@ -59,11 +59,41 @@ export const pathMap: PathMapItem[] = [
|
|||
],
|
||||
},
|
||||
{
|
||||
key: 'API_TEST_MANAGEMENT', // 接口测试-接口管理
|
||||
key: 'API_TEST_MANAGEMENT', // 接口测试-接口定义
|
||||
locale: 'menu.apiTest.management',
|
||||
route: RouteEnum.API_TEST_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
children: [
|
||||
{
|
||||
key: 'API_TEST_MANAGEMENT_MODULE', // 接口测试-接口定义-模块
|
||||
locale: 'common.module',
|
||||
route: RouteEnum.API_TEST_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'API_TEST_MANAGEMENT_DEFINITION', // 接口测试-接口定义
|
||||
locale: 'menu.apiTest.management.definition',
|
||||
route: RouteEnum.API_TEST_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'API_TEST_MANAGEMENT_MOCK', // 接口测试-接口定义-mock
|
||||
locale: 'MOCK',
|
||||
route: RouteEnum.API_TEST_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'API_TEST_MANAGEMENT_CASE', // 接口测试-接口定义-case
|
||||
locale: 'CASE',
|
||||
route: RouteEnum.API_TEST_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'API_TEST_REPORT', // 接口测试-接口测试报告
|
||||
|
|
|
@ -213,3 +213,19 @@ export enum RequestCaseStatus {
|
|||
PROCESSING = 'PROCESSING',
|
||||
DONE = 'DONE',
|
||||
}
|
||||
// 创建接口场景组成部分
|
||||
export enum ScenarioCreateComposition {
|
||||
STEP = 'STEP',
|
||||
PARAMS = 'PARAMS',
|
||||
PRE_POST = 'PRE_POST',
|
||||
ASSERTION = 'ASSERTION',
|
||||
SETTING = 'SETTING',
|
||||
}
|
||||
// 接口场景详情组成部分
|
||||
export enum ScenarioDetailComposition {
|
||||
BASE_INFO = 'BASE_INFO',
|
||||
EXECUTE_HISTORY = 'EXECUTE_HISTORY',
|
||||
CHANGE_HISTORY = 'CHANGE_HISTORY',
|
||||
DEPENDENCY = 'DEPENDENCY',
|
||||
QUOTE = 'QUOTE',
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
|||
'menu.apiTest.debug': 'API debug',
|
||||
'menu.apiTest.debug.debug': 'Debug',
|
||||
'menu.apiTest.management': 'API Management',
|
||||
'menu.apiTest.management.definition': 'API Definition',
|
||||
'menu.apiTest.scenario': 'API Scenario',
|
||||
'menu.apiTest.report': 'API Report',
|
||||
'menu.uiTest': 'UI Test',
|
||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
|||
'menu.apiTest.debug': '接口调试',
|
||||
'menu.apiTest.debug.debug': '调试',
|
||||
'menu.apiTest.management': '接口管理',
|
||||
'menu.apiTest.management.definition': '接口定义',
|
||||
'menu.apiTest.api': 'API列表',
|
||||
'menu.apiTest.scenario': '接口场景',
|
||||
'menu.apiTest.report': '接口报告',
|
||||
|
|
|
@ -483,6 +483,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO:代码拆分,结构优化
|
||||
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
|
@ -562,6 +563,7 @@
|
|||
mode?: 'definition' | 'debug';
|
||||
executeLoading: boolean; // 执行中loading
|
||||
isCopy?: boolean; // 是否是复制
|
||||
isExecute?: boolean; // 是否是执行
|
||||
}
|
||||
export type RequestParam = ExecuteApiRequestFullParams & {
|
||||
responseDefinition?: ResponseItem[];
|
||||
|
@ -603,18 +605,6 @@
|
|||
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
const isInitPluginForm = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.request.id,
|
||||
() => {
|
||||
if (temporaryResponseMap[props.request.reportId]) {
|
||||
// 如果有缓存的报告未读取,则直接赋值
|
||||
requestVModel.value.response = temporaryResponseMap[props.request.reportId];
|
||||
requestVModel.value.executeLoading = false;
|
||||
delete temporaryResponseMap[props.request.reportId];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleActiveDebugChange() {
|
||||
if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) {
|
||||
// 如果是因为加载详情触发的change则不需要标记为未保存;或者是插件协议的话需要等待表单初始化完毕
|
||||
|
@ -878,25 +868,6 @@
|
|||
handleActiveDebugChange();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => requestVModel.value.id,
|
||||
async () => {
|
||||
if (requestVModel.value.protocol !== 'HTTP') {
|
||||
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||
if (protocolOptions.value.length === 0) {
|
||||
// 还没初始化过协议列表,则初始化;在这里初始化是为了阻塞脚本的初始化,避免脚本初始化时协议列表还没初始化
|
||||
await initProtocolList();
|
||||
}
|
||||
initPluginScript();
|
||||
} else {
|
||||
initProtocolList();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理url输入框变化,解析成参数表格
|
||||
*/
|
||||
|
@ -994,35 +965,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const reportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(executeType?: 'localExec' | 'serverExec') {
|
||||
websocket.value = getSocket(
|
||||
reportId.value,
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl.value : ''
|
||||
);
|
||||
websocket.value.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (requestVModel.value.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
requestVModel.value.response = data.taskResult;
|
||||
requestVModel.value.executeLoading = false;
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryResponseMap[data.reportId] = data.taskResult;
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const saveModalVisible = ref(false);
|
||||
const saveModalForm = ref({
|
||||
name: '',
|
||||
|
@ -1061,6 +1003,38 @@
|
|||
return conditionCopy;
|
||||
}
|
||||
|
||||
const reportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(executeType?: 'localExec' | 'serverExec') {
|
||||
websocket.value = getSocket(
|
||||
reportId.value,
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl.value : ''
|
||||
);
|
||||
websocket.value.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (requestVModel.value.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
requestVModel.value.response = data.taskResult;
|
||||
requestVModel.value.executeLoading = false;
|
||||
requestVModel.value.isExecute = false;
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryResponseMap[data.reportId] = data.taskResult;
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
requestVModel.value.executeLoading = false;
|
||||
requestVModel.value.isExecute = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求参数
|
||||
* @param executeType 执行类型,执行时传入
|
||||
|
@ -1214,6 +1188,34 @@
|
|||
requestVModel.value.executeLoading = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => requestVModel.value.id,
|
||||
async () => {
|
||||
if (requestVModel.value.protocol !== 'HTTP') {
|
||||
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||
if (protocolOptions.value.length === 0) {
|
||||
// 还没初始化过协议列表,则初始化;在这里初始化是为了阻塞脚本的初始化,避免脚本初始化时协议列表还没初始化
|
||||
await initProtocolList();
|
||||
}
|
||||
await initPluginScript();
|
||||
} else {
|
||||
await initProtocolList();
|
||||
}
|
||||
if (props.request.isExecute && !requestVModel.value.executeLoading) {
|
||||
// 如果是执行操作打开接口详情,且该接口不在执行状态中,则立即执行
|
||||
execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');
|
||||
} else if (temporaryResponseMap[props.request.reportId]) {
|
||||
// 如果有缓存的报告未读取,则直接赋值
|
||||
requestVModel.value.response = temporaryResponseMap[props.request.reportId];
|
||||
requestVModel.value.executeLoading = false;
|
||||
delete temporaryResponseMap[props.request.reportId];
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function updateRequest() {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
|
@ -1266,7 +1268,6 @@
|
|||
requestVModel.value.label = res.name;
|
||||
requestVModel.value.url = res.path;
|
||||
requestVModel.value.path = res.path;
|
||||
console.log('requestVModel.value', requestVModel.value);
|
||||
if (!props.isDefinition) {
|
||||
saveModalVisible.value = false;
|
||||
}
|
||||
|
|
|
@ -265,7 +265,12 @@
|
|||
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
|
||||
break;
|
||||
case 'copy':
|
||||
addResponseTab({ ..._tab, label: `${_tab.label || _tab.name}-Copy`, name: `${_tab.label || _tab.name}-Copy` });
|
||||
addResponseTab({
|
||||
..._tab,
|
||||
id: new Date().getTime(),
|
||||
label: `copy-${t(_tab.label || _tab.name)}`,
|
||||
name: `copy-${t(_tab.label || _tab.name)}`,
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
_tab.showPopConfirm = true;
|
||||
|
|
|
@ -89,6 +89,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @description 接口测试-接口调试
|
||||
*/
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="['p-[0_16px_16px_16px]', props.class]">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="mb-[16px] flex items-center justify-end">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
|
@ -110,7 +110,7 @@
|
|||
</a-select>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<MsButton type="text" class="!mr-0">
|
||||
<MsButton type="text" class="!mr-0" @click="executeDefinition(record)">
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
|
@ -277,7 +277,7 @@
|
|||
readOnly?: boolean; // 是否是只读模式
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'openApiTab', record: ApiDefinitionDetail): void;
|
||||
(e: 'openApiTab', record: ApiDefinitionDetail, isExecute?: boolean): void;
|
||||
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
|
||||
}>();
|
||||
|
||||
|
@ -759,6 +759,10 @@
|
|||
emit('openCopyApiTab', record);
|
||||
}
|
||||
|
||||
function executeDefinition(record: ApiDefinitionDetail) {
|
||||
emit('openApiTab', record, true);
|
||||
}
|
||||
|
||||
// 拖拽排序
|
||||
async function handleTableDragSort(params: DragSortParams) {
|
||||
try {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:active-module="props.activeModule"
|
||||
:offspring-ids="props.offspringIds"
|
||||
:protocol="props.protocol"
|
||||
@open-api-tab="openApiTab"
|
||||
@open-api-tab="(record, isExecute) => openApiTab(record, false, isExecute)"
|
||||
@open-copy-api-tab="openApiTab($event, true)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -107,7 +107,6 @@
|
|||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
protocol: string;
|
||||
}>();
|
||||
const emit = defineEmits(['addDone']);
|
||||
|
||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||
|
||||
|
@ -157,6 +156,7 @@
|
|||
},
|
||||
rawBody: { value: '' },
|
||||
};
|
||||
// 调试返回的响应内容
|
||||
const defaultResponse: RequestTaskResult = {
|
||||
requestResults: [
|
||||
{
|
||||
|
@ -182,7 +182,7 @@
|
|||
},
|
||||
],
|
||||
console: '',
|
||||
}; // 调试返回的响应内容
|
||||
};
|
||||
const defaultDefinitionParams: RequestParam = {
|
||||
id: initDefaultId,
|
||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||
|
@ -276,20 +276,24 @@
|
|||
);
|
||||
|
||||
const loading = ref(false);
|
||||
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false) {
|
||||
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false, isExecute = false) {
|
||||
const isLoadedTabIndex = apiTabs.value.findIndex(
|
||||
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
|
||||
);
|
||||
if (isLoadedTabIndex > -1 && !isCopy) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
|
||||
activeApiTab.value = {
|
||||
...(apiTabs.value[isLoadedTabIndex] as RequestParam),
|
||||
isExecute,
|
||||
mode: isExecute ? 'debug' : 'definition',
|
||||
};
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
||||
const name = isCopy ? `${res.name}-copy` : res.name;
|
||||
definitionActiveKey.value = isCopy ? 'definition' : 'preview';
|
||||
const name = isCopy ? `copy-${res.name}` : res.name;
|
||||
definitionActiveKey.value = isCopy || isExecute ? 'definition' : 'preview';
|
||||
let parseRequestBodyResult;
|
||||
if (res.protocol === 'HTTP') {
|
||||
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
|
@ -305,6 +309,9 @@
|
|||
isNew: isCopy,
|
||||
unSaved: isCopy,
|
||||
isCopy,
|
||||
id: isCopy ? new Date().getTime() : res.id,
|
||||
isExecute,
|
||||
mode: isExecute ? 'debug' : 'definition',
|
||||
...parseRequestBodyResult,
|
||||
});
|
||||
nextTick(() => {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
@change-protocol="handleProtocolChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="b-0 absolute w-full p-[9px]">
|
||||
<div class="w-full p-[8px]">
|
||||
<a-divider class="!my-0 !mb-0" />
|
||||
<div class="case h-[38px]">
|
||||
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
|
||||
|
@ -52,6 +52,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @description 接口测试-接口管理
|
||||
*/
|
||||
import { provide } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> assertion </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> changeHistory </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> dependency </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> executeHistory </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> params </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> prePost </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> quote </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> setting </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> step </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> create </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<div class="h-full w-full overflow-hidden">
|
||||
<div class="px-[24px] pt-[16px]">
|
||||
<MsDetailCard :title="`【${previewDetail.num}】${previewDetail.name}`" :description="description">
|
||||
<template #titleAppend>
|
||||
<apiStatus :status="previewDetail.status" size="small" />
|
||||
</template>
|
||||
<template #titleRight>
|
||||
<a-button
|
||||
type="outline"
|
||||
:loading="followLoading"
|
||||
size="mini"
|
||||
class="arco-btn-outline--secondary mr-[4px] !bg-transparent"
|
||||
@click="toggleFollowReview"
|
||||
>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<MsIcon
|
||||
:type="previewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="`${previewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
|
||||
:size="14"
|
||||
/>
|
||||
{{ t(previewDetail.follow ? 'common.forked' : 'common.fork') }}
|
||||
</div>
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" class="arco-btn-outline--secondary !bg-transparent" @click="share">
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<MsIcon type="icon-icon_share1" class="text-[var(--color-text-4)]" :size="14" />
|
||||
{{ t('common.share') }}
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #type="{ value }">
|
||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
</div>
|
||||
<div class="h-[calc(100%-124px)]">
|
||||
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.BASE_INFO"
|
||||
:title="t('apiScenario.baseInfo')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
BASE_INFO
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]">
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioCreateComposition.PARAMS"
|
||||
:title="t('apiScenario.params')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioCreateComposition.PRE_POST"
|
||||
:title="t('apiScenario.prePost')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioCreateComposition.ASSERTION"
|
||||
:title="t('apiScenario.assertion')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.EXECUTE_HISTORY"
|
||||
:title="t('apiScenario.executeHistory')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<executeHistory v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.CHANGE_HISTORY"
|
||||
:title="t('apiScenario.changeHistory')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.DEPENDENCY"
|
||||
:title="t('apiScenario.dependency')"
|
||||
class="px-[24px] py-[16px]"
|
||||
>
|
||||
<dependency v-if="activeKey === ScenarioDetailComposition.DEPENDENCY" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="ScenarioDetailComposition.QUOTE" :title="t('apiScenario.quote')" class="px-[24px] py-[16px]">
|
||||
<quote v-if="activeKey === ScenarioDetailComposition.QUOTE" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="px-[24px] py-[16px]">
|
||||
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
import { RequestMethods, ScenarioCreateComposition, ScenarioDetailComposition } from '@/enums/apiEnum';
|
||||
|
||||
// 组成部分异步导入
|
||||
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
||||
const params = defineAsyncComponent(() => import('../components/params.vue'));
|
||||
const prePost = defineAsyncComponent(() => import('../components/prePost.vue'));
|
||||
const assertion = defineAsyncComponent(() => import('../components/assertion.vue'));
|
||||
const executeHistory = defineAsyncComponent(() => import('../components/executeHistory.vue'));
|
||||
const changeHistory = defineAsyncComponent(() => import('../components/changeHistory.vue'));
|
||||
const dependency = defineAsyncComponent(() => import('../components/dependency.vue'));
|
||||
const quote = defineAsyncComponent(() => import('../components/quote.vue'));
|
||||
const setting = defineAsyncComponent(() => import('../components/setting.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
detail: RequestParam;
|
||||
}>();
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
||||
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
|
||||
|
||||
const description = computed(() => [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'something.type',
|
||||
value: 'type',
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'something.path',
|
||||
value: 'path',
|
||||
},
|
||||
]);
|
||||
|
||||
const followLoading = ref(false);
|
||||
async function toggleFollowReview() {
|
||||
try {
|
||||
followLoading.value = true;
|
||||
Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
|
||||
emit('updateFollow');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
followLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function share() {
|
||||
if (isSupported) {
|
||||
copy(`${window.location.href}&dId=${previewDetail.value.id}`);
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
Message.error(t('common.copyNotSupport'));
|
||||
}
|
||||
}
|
||||
|
||||
const activeKey = ref<ScenarioCreateComposition | ScenarioDetailComposition>(ScenarioDetailComposition.BASE_INFO);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-tabs-content) {
|
||||
@apply pt-0;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,23 @@
|
|||
<template>
|
||||
<div class="rounded-2xl bg-white">
|
||||
<div class="p-[24px] pb-[16px]">
|
||||
<span>场景列表接口(标签页配置未实现)</span>
|
||||
<MsCard no-content-padding simple>
|
||||
<div class="p-[24px_24px_8px_24px]">
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeApiTab"
|
||||
v-model:tabs="apiTabs"
|
||||
class="flex-1 overflow-hidden"
|
||||
@add="newTab"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
<a-tooltip :content="tab.label" :mouse-enter-delay="500">
|
||||
<div class="one-line-text max-w-[144px]">
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
</div>
|
||||
<a-divider class="!my-0" />
|
||||
<div class="pageWrap">
|
||||
<div v-if="activeApiTab.id === 'all'" class="pageWrap">
|
||||
<MsSplitBox :size="300" :max="0.5">
|
||||
<template #first>
|
||||
<div class="p-[24px] pb-0">
|
||||
|
@ -36,7 +49,8 @@
|
|||
</template>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
</div>
|
||||
<detail v-else :detail="activeApiTab"></detail>
|
||||
</MsCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -46,9 +60,12 @@
|
|||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import scenarioModuleTree from './components/scenarioModuleTree.vue';
|
||||
import detail from './detail/index.vue';
|
||||
import ApiTable from '@/views/api-test/management/components/management/api/apiTable.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
@ -56,6 +73,25 @@
|
|||
import { ModuleTreeNode } from '@/models/common';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const apiTabs = ref<any[]>([
|
||||
{
|
||||
id: 'all',
|
||||
label: t('apiScenario.allScenario'),
|
||||
closable: false,
|
||||
},
|
||||
]);
|
||||
const activeApiTab = ref<any>(apiTabs.value[0]);
|
||||
|
||||
function newTab() {
|
||||
apiTabs.value.push({
|
||||
id: `newTab${apiTabs.value.length}`,
|
||||
label: `New Tab ${apiTabs.value.length}`,
|
||||
closable: true,
|
||||
});
|
||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
}
|
||||
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
const folderTreePathMap = ref<Record<string, any>>({});
|
||||
const activeFolder = ref<string>('all');
|
||||
|
@ -86,8 +122,7 @@
|
|||
|
||||
<style scoped lang="less">
|
||||
.pageWrap {
|
||||
min-width: 1000px;
|
||||
height: calc(100vh - 166px);
|
||||
height: calc(100% - 65px);
|
||||
border-radius: var(--border-radius-large);
|
||||
@apply bg-white;
|
||||
.case {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export default {
|
||||
'apiScenario.allScenario': 'All scenarios',
|
||||
'apiScenario.createScenario': 'Create scenario',
|
||||
'apiScenario.importScenario': 'Import scenario',
|
||||
'apiScenario.tree.selectorPlaceholder': 'Please enter the module name',
|
||||
|
@ -6,15 +7,20 @@ export default {
|
|||
'apiScenario.tree.showLeafNodeScenario': 'Show subdirectory scenarios',
|
||||
'apiScenario.tree.recycleBin': 'Recycle bin',
|
||||
'apiScenario.tree.noMatchModule': 'No matching module/scene yet',
|
||||
|
||||
'apiScenario.createSubModule': 'Create sub-module',
|
||||
|
||||
'apiScenario.module.deleteTipTitle': 'Delete {name} module?',
|
||||
'apiScenario.module.deleteTipContent':
|
||||
'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.',
|
||||
|
||||
'apiScenario.deleteConfirm': 'Confirm',
|
||||
'apiScenario.deleteSuccess': 'Success',
|
||||
|
||||
'apiScenario.moveSuccess': 'Success',
|
||||
'apiScenario.baseInfo': 'Base info',
|
||||
'apiScenario.step': 'Step',
|
||||
'apiScenario.params': 'Params',
|
||||
'apiScenario.prePost': 'Pre/Post',
|
||||
'apiScenario.assertion': 'Assertion',
|
||||
'apiScenario.executeHistory': 'Execute history',
|
||||
'apiScenario.changeHistory': 'Change history',
|
||||
'apiScenario.dependency': 'Dependencies',
|
||||
'apiScenario.quote': 'Reference',
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export default {
|
||||
'apiScenario.allScenario': '全部场景',
|
||||
'apiScenario.createScenario': '新建场景',
|
||||
'apiScenario.importScenario': '导入场景',
|
||||
'apiScenario.tree.selectorPlaceholder': '请输入模块名称',
|
||||
|
@ -6,14 +7,19 @@ export default {
|
|||
'apiScenario.tree.showLeafNodeScenario': '显示子目录场景',
|
||||
'apiScenario.tree.recycleBin': '回收站',
|
||||
'apiScenario.tree.noMatchModule': '暂无匹配的模块/场景',
|
||||
|
||||
'apiScenario.createSubModule': '新建子模块',
|
||||
|
||||
'apiScenario.module.deleteTipTitle': '是否删除 {name} 模块?',
|
||||
'apiScenario.module.deleteTipContent': '删除后,会同步删除模块下的所有场景,请谨慎操作.',
|
||||
|
||||
'apiScenario.deleteConfirm': '确认删除',
|
||||
'apiScenario.deleteSuccess': '删除成功',
|
||||
|
||||
'apiScenario.moveSuccess': '移动成功',
|
||||
'apiScenario.baseInfo': '基本信息',
|
||||
'apiScenario.step': '步骤',
|
||||
'apiScenario.params': '参数',
|
||||
'apiScenario.prePost': '前/后置',
|
||||
'apiScenario.assertion': '断言',
|
||||
'apiScenario.executeHistory': '执行历史',
|
||||
'apiScenario.changeHistory': '变更历史',
|
||||
'apiScenario.dependency': '依赖关系',
|
||||
'apiScenario.quote': '引用关系',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue