feat(接口场景): 场景大框架&接口定义列表执行

This commit is contained in:
baiqi 2024-03-13 17:56:00 +08:00 committed by 刘瑞斌
parent f8343ea765
commit 1c86838b8b
25 changed files with 463 additions and 90 deletions

View File

@ -32,7 +32,12 @@
</slot> </slot>
</div> </div>
</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]"> <div v-if="isExpand" class="flex items-center gap-[4px]">
{{ t('msDetailCard.collapse') }} {{ t('msDetailCard.collapse') }}
<icon-up :size="14" /> <icon-up :size="14" />

View File

@ -59,11 +59,41 @@ export const pathMap: PathMapItem[] = [
], ],
}, },
{ {
key: 'API_TEST_MANAGEMENT', // 接口测试-接口管理 key: 'API_TEST_MANAGEMENT', // 接口测试-接口定义
locale: 'menu.apiTest.management', locale: 'menu.apiTest.management',
route: RouteEnum.API_TEST_MANAGEMENT, route: RouteEnum.API_TEST_MANAGEMENT,
permission: [], permission: [],
level: MENU_LEVEL[2], 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', // 接口测试-接口测试报告 key: 'API_TEST_REPORT', // 接口测试-接口测试报告

View File

@ -213,3 +213,19 @@ export enum RequestCaseStatus {
PROCESSING = 'PROCESSING', PROCESSING = 'PROCESSING',
DONE = 'DONE', 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',
}

View File

@ -27,6 +27,7 @@ export default {
'menu.apiTest.debug': 'API debug', 'menu.apiTest.debug': 'API debug',
'menu.apiTest.debug.debug': 'Debug', 'menu.apiTest.debug.debug': 'Debug',
'menu.apiTest.management': 'API Management', 'menu.apiTest.management': 'API Management',
'menu.apiTest.management.definition': 'API Definition',
'menu.apiTest.scenario': 'API Scenario', 'menu.apiTest.scenario': 'API Scenario',
'menu.apiTest.report': 'API Report', 'menu.apiTest.report': 'API Report',
'menu.uiTest': 'UI Test', 'menu.uiTest': 'UI Test',

View File

@ -27,6 +27,7 @@ export default {
'menu.apiTest.debug': '接口调试', 'menu.apiTest.debug': '接口调试',
'menu.apiTest.debug.debug': '调试', 'menu.apiTest.debug.debug': '调试',
'menu.apiTest.management': '接口管理', 'menu.apiTest.management': '接口管理',
'menu.apiTest.management.definition': '接口定义',
'menu.apiTest.api': 'API列表', 'menu.apiTest.api': 'API列表',
'menu.apiTest.scenario': '接口场景', 'menu.apiTest.scenario': '接口场景',
'menu.apiTest.report': '接口报告', 'menu.apiTest.report': '接口报告',

View File

@ -483,6 +483,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO:
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue'; import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es'; import { cloneDeep, debounce } from 'lodash-es';
@ -562,6 +563,7 @@
mode?: 'definition' | 'debug'; mode?: 'definition' | 'debug';
executeLoading: boolean; // loading executeLoading: boolean; // loading
isCopy?: boolean; // isCopy?: boolean; //
isExecute?: boolean; //
} }
export type RequestParam = ExecuteApiRequestFullParams & { export type RequestParam = ExecuteApiRequestFullParams & {
responseDefinition?: ResponseItem[]; responseDefinition?: ResponseItem[];
@ -603,18 +605,6 @@
const temporaryResponseMap = {}; // websockettab const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false); 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() { function handleActiveDebugChange() {
if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) { if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) {
// change // change
@ -878,25 +868,6 @@
handleActiveDebugChange(); 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输入框变化解析成参数表格 * 处理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) {
// tabtab
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 saveModalVisible = ref(false);
const saveModalForm = ref({ const saveModalForm = ref({
name: '', name: '',
@ -1061,6 +1003,38 @@
return conditionCopy; 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) {
// tabtab
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 执行类型执行时传入 * @param executeType 执行类型执行时传入
@ -1214,6 +1188,34 @@
requestVModel.value.executeLoading = false; 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() { async function updateRequest() {
try { try {
saveLoading.value = true; saveLoading.value = true;
@ -1266,7 +1268,6 @@
requestVModel.value.label = res.name; requestVModel.value.label = res.name;
requestVModel.value.url = res.path; requestVModel.value.url = res.path;
requestVModel.value.path = res.path; requestVModel.value.path = res.path;
console.log('requestVModel.value', requestVModel.value);
if (!props.isDefinition) { if (!props.isDefinition) {
saveModalVisible.value = false; saveModalVisible.value = false;
} }

View File

@ -265,7 +265,12 @@
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click')); document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
break; break;
case 'copy': 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; break;
case 'delete': case 'delete':
_tab.showPopConfirm = true; _tab.showPopConfirm = true;

View File

@ -89,6 +89,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
/**
* @description 接口测试-接口调试
*/
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';

View File

@ -1,6 +1,6 @@
<template> <template>
<div :class="['p-[0_16px_16px_16px]', props.class]"> <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]"> <div class="flex items-center gap-[8px]">
<a-input-search <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
@ -110,7 +110,7 @@
</a-select> </a-select>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<MsButton type="text" class="!mr-0"> <MsButton type="text" class="!mr-0" @click="executeDefinition(record)">
{{ t('apiTestManagement.execute') }} {{ t('apiTestManagement.execute') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
@ -277,7 +277,7 @@
readOnly?: boolean; // readOnly?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'openApiTab', record: ApiDefinitionDetail): void; (e: 'openApiTab', record: ApiDefinitionDetail, isExecute?: boolean): void;
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void; (e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
}>(); }>();
@ -759,6 +759,10 @@
emit('openCopyApiTab', record); emit('openCopyApiTab', record);
} }
function executeDefinition(record: ApiDefinitionDetail) {
emit('openApiTab', record, true);
}
// //
async function handleTableDragSort(params: DragSortParams) { async function handleTableDragSort(params: DragSortParams) {
try { try {

View File

@ -6,7 +6,7 @@
:active-module="props.activeModule" :active-module="props.activeModule"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="props.protocol" :protocol="props.protocol"
@open-api-tab="openApiTab" @open-api-tab="(record, isExecute) => openApiTab(record, false, isExecute)"
@open-copy-api-tab="openApiTab($event, true)" @open-copy-api-tab="openApiTab($event, true)"
/> />
</div> </div>
@ -107,7 +107,6 @@
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
protocol: string; protocol: string;
}>(); }>();
const emit = defineEmits(['addDone']);
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
@ -157,6 +156,7 @@
}, },
rawBody: { value: '' }, rawBody: { value: '' },
}; };
//
const defaultResponse: RequestTaskResult = { const defaultResponse: RequestTaskResult = {
requestResults: [ requestResults: [
{ {
@ -182,7 +182,7 @@
}, },
], ],
console: '', console: '',
}; // };
const defaultDefinitionParams: RequestParam = { const defaultDefinitionParams: RequestParam = {
id: initDefaultId, id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule, moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
@ -276,20 +276,24 @@
); );
const loading = ref(false); 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( const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id) (e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
); );
if (isLoadedTabIndex > -1 && !isCopy) { if (isLoadedTabIndex > -1 && !isCopy) {
// tabtab // tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam; activeApiTab.value = {
...(apiTabs.value[isLoadedTabIndex] as RequestParam),
isExecute,
mode: isExecute ? 'debug' : 'definition',
};
return; return;
} }
try { try {
loading.value = true; loading.value = true;
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id); const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
const name = isCopy ? `${res.name}-copy` : res.name; const name = isCopy ? `copy-${res.name}` : res.name;
definitionActiveKey.value = isCopy ? 'definition' : 'preview'; definitionActiveKey.value = isCopy || isExecute ? 'definition' : 'preview';
let parseRequestBodyResult; let parseRequestBodyResult;
if (res.protocol === 'HTTP') { if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
@ -305,6 +309,9 @@
isNew: isCopy, isNew: isCopy,
unSaved: isCopy, unSaved: isCopy,
isCopy, isCopy,
id: isCopy ? new Date().getTime() : res.id,
isExecute,
mode: isExecute ? 'debug' : 'definition',
...parseRequestBodyResult, ...parseRequestBodyResult,
}); });
nextTick(() => { nextTick(() => {

View File

@ -14,7 +14,7 @@
@change-protocol="handleProtocolChange" @change-protocol="handleProtocolChange"
/> />
</div> </div>
<div class="b-0 absolute w-full p-[9px]"> <div class="w-full p-[8px]">
<a-divider class="!my-0 !mb-0" /> <a-divider class="!my-0 !mb-0" />
<div class="case h-[38px]"> <div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')"> <div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
@ -52,6 +52,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
/**
* @description 接口测试-接口管理
*/
import { provide } from 'vue'; import { provide } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';

View File

@ -0,0 +1,7 @@
<template>
<div> assertion </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> changeHistory </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> dependency </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> executeHistory </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> params </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> prePost </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> quote </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> setting </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> step </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> create </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -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>

View File

@ -1,10 +1,23 @@
<template> <template>
<div class="rounded-2xl bg-white"> <MsCard no-content-padding simple>
<div class="p-[24px] pb-[16px]"> <div class="p-[24px_24px_8px_24px]">
<span>场景列表接口(标签页配置未实现)</span> <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> </div>
<a-divider class="!my-0" /> <a-divider class="!my-0" />
<div class="pageWrap"> <div v-if="activeApiTab.id === 'all'" class="pageWrap">
<MsSplitBox :size="300" :max="0.5"> <MsSplitBox :size="300" :max="0.5">
<template #first> <template #first>
<div class="p-[24px] pb-0"> <div class="p-[24px] pb-0">
@ -36,7 +49,8 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
</div> <detail v-else :detail="activeApiTab"></detail>
</MsCard>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -46,9 +60,12 @@
import { ref } from 'vue'; 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 MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import scenarioModuleTree from './components/scenarioModuleTree.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 ApiTable from '@/views/api-test/management/components/management/api/apiTable.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -56,6 +73,25 @@
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
const { t } = useI18n(); 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 folderTree = ref<ModuleTreeNode[]>([]);
const folderTreePathMap = ref<Record<string, any>>({}); const folderTreePathMap = ref<Record<string, any>>({});
const activeFolder = ref<string>('all'); const activeFolder = ref<string>('all');
@ -86,8 +122,7 @@
<style scoped lang="less"> <style scoped lang="less">
.pageWrap { .pageWrap {
min-width: 1000px; height: calc(100% - 65px);
height: calc(100vh - 166px);
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
@apply bg-white; @apply bg-white;
.case { .case {

View File

@ -1,4 +1,5 @@
export default { export default {
'apiScenario.allScenario': 'All scenarios',
'apiScenario.createScenario': 'Create scenario', 'apiScenario.createScenario': 'Create scenario',
'apiScenario.importScenario': 'Import scenario', 'apiScenario.importScenario': 'Import scenario',
'apiScenario.tree.selectorPlaceholder': 'Please enter the module name', 'apiScenario.tree.selectorPlaceholder': 'Please enter the module name',
@ -6,15 +7,20 @@ export default {
'apiScenario.tree.showLeafNodeScenario': 'Show subdirectory scenarios', 'apiScenario.tree.showLeafNodeScenario': 'Show subdirectory scenarios',
'apiScenario.tree.recycleBin': 'Recycle bin', 'apiScenario.tree.recycleBin': 'Recycle bin',
'apiScenario.tree.noMatchModule': 'No matching module/scene yet', 'apiScenario.tree.noMatchModule': 'No matching module/scene yet',
'apiScenario.createSubModule': 'Create sub-module', 'apiScenario.createSubModule': 'Create sub-module',
'apiScenario.module.deleteTipTitle': 'Delete {name} module?', 'apiScenario.module.deleteTipTitle': 'Delete {name} module?',
'apiScenario.module.deleteTipContent': 'apiScenario.module.deleteTipContent':
'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.', 'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.',
'apiScenario.deleteConfirm': 'Confirm', 'apiScenario.deleteConfirm': 'Confirm',
'apiScenario.deleteSuccess': 'Success', 'apiScenario.deleteSuccess': 'Success',
'apiScenario.moveSuccess': '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',
}; };

View File

@ -1,4 +1,5 @@
export default { export default {
'apiScenario.allScenario': '全部场景',
'apiScenario.createScenario': '新建场景', 'apiScenario.createScenario': '新建场景',
'apiScenario.importScenario': '导入场景', 'apiScenario.importScenario': '导入场景',
'apiScenario.tree.selectorPlaceholder': '请输入模块名称', 'apiScenario.tree.selectorPlaceholder': '请输入模块名称',
@ -6,14 +7,19 @@ export default {
'apiScenario.tree.showLeafNodeScenario': '显示子目录场景', 'apiScenario.tree.showLeafNodeScenario': '显示子目录场景',
'apiScenario.tree.recycleBin': '回收站', 'apiScenario.tree.recycleBin': '回收站',
'apiScenario.tree.noMatchModule': '暂无匹配的模块/场景', 'apiScenario.tree.noMatchModule': '暂无匹配的模块/场景',
'apiScenario.createSubModule': '新建子模块', 'apiScenario.createSubModule': '新建子模块',
'apiScenario.module.deleteTipTitle': '是否删除 {name} 模块?', 'apiScenario.module.deleteTipTitle': '是否删除 {name} 模块?',
'apiScenario.module.deleteTipContent': '删除后,会同步删除模块下的所有场景,请谨慎操作.', 'apiScenario.module.deleteTipContent': '删除后,会同步删除模块下的所有场景,请谨慎操作.',
'apiScenario.deleteConfirm': '确认删除', 'apiScenario.deleteConfirm': '确认删除',
'apiScenario.deleteSuccess': '删除成功', 'apiScenario.deleteSuccess': '删除成功',
'apiScenario.moveSuccess': '移动成功', 'apiScenario.moveSuccess': '移动成功',
'apiScenario.baseInfo': '基本信息',
'apiScenario.step': '步骤',
'apiScenario.params': '参数',
'apiScenario.prePost': '前/后置',
'apiScenario.assertion': '断言',
'apiScenario.executeHistory': '执行历史',
'apiScenario.changeHistory': '变更历史',
'apiScenario.dependency': '依赖关系',
'apiScenario.quote': '引用关系',
}; };