fix(接口测试): 定义和场景的列表操作及详情新增编辑按钮&调整样式
This commit is contained in:
parent
655ea48f14
commit
6bccc40e9c
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="ms-detail-card">
|
||||
<div class="ms-detail-card-title flex items-center justify-between">
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-tooltip :content="t(props.title)">
|
||||
<div class="one-line-text flex-1 font-medium text-[var(--color-text-1)]">
|
||||
{{ t(props.title) }}
|
||||
|
|
|
@ -1592,6 +1592,8 @@
|
|||
});
|
||||
|
||||
defineExpose({
|
||||
execute,
|
||||
isPriorityLocalExec,
|
||||
makeRequestParams,
|
||||
changeVerticalExpand,
|
||||
});
|
||||
|
|
|
@ -234,7 +234,6 @@
|
|||
* 响应状态码对应颜色
|
||||
*/
|
||||
const statusCodeColor = computed(() => {
|
||||
debugger;
|
||||
if (activeStepDetailCopy.value?.content) {
|
||||
const code = Number(activeStepDetailCopy.value?.content?.responseResult.responseCode);
|
||||
if (code >= 200 && code < 300) {
|
||||
|
|
|
@ -129,6 +129,15 @@
|
|||
<apiStatus v-else :status="record.status" size="small" />
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']"
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
@click="editDefinition(record)"
|
||||
>
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<a-divider v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']" direction="vertical" :margin="8"></a-divider>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+EXECUTE']"
|
||||
type="text"
|
||||
|
@ -343,6 +352,7 @@
|
|||
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
|
||||
(e: 'addApiTab'): void;
|
||||
(e: 'import'): void;
|
||||
(e: 'openEditApiTab', record: ApiDefinitionDetail, isCopy: boolean, isExecute: boolean, isEdit: boolean): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
@ -445,7 +455,7 @@
|
|||
slotName: 'action',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: hasOperationPermission.value ? 150 : 50,
|
||||
width: hasOperationPermission.value ? 200 : 50,
|
||||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||
|
@ -872,6 +882,10 @@
|
|||
emit('openApiTab', record, true);
|
||||
}
|
||||
|
||||
function editDefinition(record: ApiDefinitionDetail) {
|
||||
emit('openEditApiTab', record, false, false, true);
|
||||
}
|
||||
|
||||
// 拖拽排序
|
||||
async function handleTableDragSort(params: DragSortParams) {
|
||||
try {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
@open-copy-api-tab="openApiTab($event, true)"
|
||||
@add-api-tab="addApiTab"
|
||||
@import="emit('import')"
|
||||
@open-edit-api-tab="openApiTab"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
||||
|
@ -20,6 +21,34 @@
|
|||
class="ms-api-tab-nav"
|
||||
@change="changeDefinitionActiveKey"
|
||||
>
|
||||
<template v-if="activeApiTab.definitionActiveKey === 'preview'" #extra>
|
||||
<div class="flex gap-[12px] pr-[16px]">
|
||||
<a-button
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+EXECUTE']"
|
||||
type="primary"
|
||||
@click="toExecuteDefinition"
|
||||
>
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</a-button>
|
||||
<a-dropdown-button type="outline" @click="toEditDefinition">
|
||||
{{ t('common.edit') }}
|
||||
<template #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template #content>
|
||||
<a-doption
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+DELETE']"
|
||||
value="delete"
|
||||
class="error-6 text-[rgb(var(--danger-6))]"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
{{ t('common.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-tab-pane
|
||||
v-if="!activeApiTab.isNew"
|
||||
key="preview"
|
||||
|
@ -36,6 +65,7 @@
|
|||
</a-tab-pane>
|
||||
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane">
|
||||
<requestComposition
|
||||
ref="requestCompositionRef"
|
||||
v-model:detail-loading="loading"
|
||||
v-model:request="activeApiTab"
|
||||
:module-tree="props.moduleTree"
|
||||
|
@ -77,7 +107,6 @@
|
|||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
// import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import caseTable from '../case/caseTable.vue';
|
||||
|
@ -88,6 +117,7 @@
|
|||
import {
|
||||
addDefinition,
|
||||
debugDefinition,
|
||||
deleteDefinition,
|
||||
getDefinitionDetail,
|
||||
getTransferOptions,
|
||||
transferFile,
|
||||
|
@ -95,6 +125,7 @@
|
|||
uploadTempFile,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ProtocolItem } from '@/models/apiTest/common';
|
||||
|
@ -126,6 +157,7 @@
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'deleteApi', id: string): void;
|
||||
(e: 'import'): void;
|
||||
}>();
|
||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||
|
@ -134,6 +166,7 @@
|
|||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
|
||||
required: true,
|
||||
|
@ -254,7 +287,12 @@
|
|||
);
|
||||
|
||||
const loading = ref(false);
|
||||
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false, isExecute = false) {
|
||||
async function openApiTab(
|
||||
apiInfo: ModuleTreeNode | ApiDefinitionDetail | string,
|
||||
isCopy = false,
|
||||
isExecute = false,
|
||||
isEdit = false
|
||||
) {
|
||||
const isLoadedTabIndex = apiTabs.value.findIndex(
|
||||
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
|
||||
);
|
||||
|
@ -262,6 +300,7 @@
|
|||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
activeApiTab.value = {
|
||||
...(apiTabs.value[isLoadedTabIndex] as RequestParam),
|
||||
definitionActiveKey: isCopy || isExecute || isEdit ? 'definition' : 'preview',
|
||||
isExecute,
|
||||
mode: isExecute ? 'debug' : 'definition',
|
||||
};
|
||||
|
@ -289,7 +328,7 @@
|
|||
id: isCopy ? new Date().getTime() : res.id,
|
||||
isExecute,
|
||||
mode: isExecute ? 'debug' : 'definition',
|
||||
definitionActiveKey: isCopy || isExecute ? 'definition' : 'preview',
|
||||
definitionActiveKey: isCopy || isExecute || isEdit ? 'definition' : 'preview',
|
||||
...parseRequestBodyResult,
|
||||
});
|
||||
nextTick(() => {
|
||||
|
@ -320,6 +359,45 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 跳转到接口定义tab
|
||||
function toEditDefinition() {
|
||||
activeApiTab.value.definitionActiveKey = 'definition';
|
||||
activeApiTab.value.mode = 'definition';
|
||||
}
|
||||
|
||||
// 跳转到接口定义tab,且执行
|
||||
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
|
||||
function toExecuteDefinition() {
|
||||
activeApiTab.value.definitionActiveKey = 'definition';
|
||||
activeApiTab.value.isExecute = true;
|
||||
activeApiTab.value.mode = 'debug';
|
||||
requestCompositionRef.value?.execute(requestCompositionRef.value?.isPriorityLocalExec ? 'localExec' : 'serverExec');
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('apiTestManagement.deleteApiTipTitle', { name: activeApiTab.value.name }),
|
||||
content: t('apiTestManagement.deleteApiTip'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
await deleteDefinition(activeApiTab.value.id as string);
|
||||
emit('deleteApi', activeApiTab.value.id as string);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openApiTab,
|
||||
addApiTab,
|
||||
|
@ -328,9 +406,15 @@
|
|||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.error-6 {
|
||||
color: rgb(var(--danger-6));
|
||||
&:hover {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
:deep(.ms-api-tab-nav) {
|
||||
@apply h-full;
|
||||
.arco-tabs-nav-tab {
|
||||
.arco-tabs-nav {
|
||||
border-bottom: 1px solid var(--color-text-n8);
|
||||
}
|
||||
.arco-tabs-content {
|
||||
|
|
|
@ -7,32 +7,17 @@
|
|||
:simple-show-count="4"
|
||||
>
|
||||
<template #titleAppend>
|
||||
<apiStatus :status="previewDetail.status" size="small" />
|
||||
</template>
|
||||
<template #titleRight>
|
||||
<a-button
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']"
|
||||
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
|
||||
v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']"
|
||||
:loading="followLoading"
|
||||
: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"
|
||||
class="cursor-pointer"
|
||||
:size="16"
|
||||
@click="toggleFollowReview"
|
||||
/>
|
||||
{{ 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>
|
||||
<MsIcon type="icon-icon_share1" class="cursor-pointer text-[var(--color-text-4)]" :size="16" @click="share" />
|
||||
<apiStatus :status="previewDetail.status" size="small" />
|
||||
</template>
|
||||
<template #type="{ value }">
|
||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
|
@ -171,6 +156,7 @@
|
|||
followLoading.value = true;
|
||||
await toggleFollowDefinition(previewDetail.value.id);
|
||||
Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
|
||||
previewDetail.value.follow = !previewDetail.value.follow;
|
||||
emit('updateFollow');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -11,39 +11,44 @@
|
|||
@stop-debug="stopDebug"
|
||||
@execute="handleExecute"
|
||||
/>
|
||||
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
|
||||
<a-button v-if="!props.isDrawer" type="outline">{{ t('common.operation') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="edit">
|
||||
<MsIcon type="icon-icon_edit_outlined" />
|
||||
<a-dropdown-button v-if="!props.isDrawer" type="outline" @click="editCase">
|
||||
{{ t('common.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="share">
|
||||
<MsIcon type="icon-icon_share1" />
|
||||
{{ t('common.share') }}
|
||||
</a-doption>
|
||||
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="fork">
|
||||
<MsIcon
|
||||
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
|
||||
/>
|
||||
{{ t('common.fork') }}
|
||||
</a-doption>
|
||||
<a-divider v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" margin="4px" />
|
||||
<template #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template #content>
|
||||
<a-doption
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
|
||||
value="delete"
|
||||
class="error-6 text-[rgb(var(--danger-6))]"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
{{ t('common.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-dropdown-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-tab-pane key="detail" :title="t('case.detail')" class="px-[18px] py-[16px]">
|
||||
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
|
||||
<template v-if="!props.isDrawer" #titleAppend>
|
||||
<MsIcon
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
|
||||
:loading="followLoading"
|
||||
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
|
||||
class="cursor-pointer"
|
||||
:size="16"
|
||||
@click="follow"
|
||||
/>
|
||||
<MsIcon
|
||||
type="icon-icon_share1"
|
||||
class="cursor-pointer text-[var(--color-text-4)]"
|
||||
:size="16"
|
||||
@click="share"
|
||||
/>
|
||||
</template>
|
||||
<template #type="{ value }">
|
||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
</template>
|
||||
|
@ -159,6 +164,7 @@
|
|||
followLoading.value = true;
|
||||
await toggleFollowCase(caseDetail.value.id);
|
||||
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
|
||||
caseDetail.value.follow = !caseDetail.value.follow;
|
||||
emit('updateFollow');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -206,25 +212,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
function handleSelect(val: string | number | Record<string, any> | undefined) {
|
||||
switch (val) {
|
||||
case 'edit':
|
||||
editCase();
|
||||
break;
|
||||
case 'share':
|
||||
share();
|
||||
break;
|
||||
case 'fork':
|
||||
follow();
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const protocols = inject<Ref<ProtocolItem[]>>('protocols');
|
||||
|
||||
const currentEnvConfigByInject = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
>
|
||||
<MsIcon
|
||||
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="[props.detail.follow ? 'text-[rgb(var(--warning-6))]' : '']"
|
||||
:class="[props.detail.follow ? '!text-[rgb(var(--warning-6))]' : '']"
|
||||
/>
|
||||
{{ t('common.fork') }}
|
||||
</MsButton>
|
||||
|
|
|
@ -167,6 +167,19 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
@click="editCase(record)"
|
||||
>
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<a-divider
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
|
||||
direction="vertical"
|
||||
:margin="8"
|
||||
></a-divider>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
|
||||
type="text"
|
||||
|
@ -335,7 +348,6 @@
|
|||
batchExecuteCase,
|
||||
deleteCase,
|
||||
dragSort,
|
||||
executeCase,
|
||||
getCaseDetail,
|
||||
getCasePage,
|
||||
updateCasePriority,
|
||||
|
@ -516,7 +528,7 @@
|
|||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: hasOperationPermission.value ? 150 : 50,
|
||||
width: hasOperationPermission.value ? 200 : 50,
|
||||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, {
|
||||
|
@ -811,16 +823,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
async function onExecute(id: string) {
|
||||
try {
|
||||
await executeCase(id);
|
||||
Message.success(t('case.detail.execute.success'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelBatchEdit() {
|
||||
showBatchEditModal.value = false;
|
||||
batchFormRef.value?.resetFields();
|
||||
|
@ -943,6 +945,11 @@
|
|||
loadCaseList();
|
||||
}
|
||||
|
||||
async function editCase(record: ApiCaseDetail) {
|
||||
await getCaseDetailInfo(record.id);
|
||||
createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, caseDetail.value as RequestParam);
|
||||
}
|
||||
|
||||
// 在api下的用例里打开用例详情抽屉,点击编辑,编辑后在此刷新数据
|
||||
function loadCase(id: string) {
|
||||
getCaseDetailInfo(id);
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
:protocol="props.protocol"
|
||||
:module-tree="props.moduleTree"
|
||||
@import="emit('import')"
|
||||
@delete-api="(id) => handleDeleteApiFromModuleTree(id)"
|
||||
/>
|
||||
<apiCase
|
||||
v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"
|
||||
|
|
|
@ -1,40 +1,110 @@
|
|||
<template>
|
||||
<MsDescription :descriptions="descriptions"> </MsDescription>
|
||||
<a-form ref="createFormRef" :model="scenario" layout="vertical">
|
||||
<a-form-item
|
||||
field="name"
|
||||
:label="t('apiScenario.name')"
|
||||
class="mb-[16px]"
|
||||
:rules="[{ required: true, message: t('apiScenario.nameRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="scenario.name"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.namePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.belongModule')" class="mb-[16px]">
|
||||
<a-tree-select
|
||||
v-model:modelValue="scenario.moduleId"
|
||||
:data="props.moduleTree"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
height: 200,
|
||||
threshold: 200,
|
||||
},
|
||||
}"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.scenarioLevel')">
|
||||
<a-select v-model:model-value="scenario.priority" :placeholder="t('common.pleaseSelect')">
|
||||
<template #label>
|
||||
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="scenario.priority" /></span>
|
||||
</template>
|
||||
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||
<caseLevel :case-level="item.label as CaseLevel" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.status')" class="mb-[16px]">
|
||||
<a-select
|
||||
v-model:model-value="scenario.status"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
>
|
||||
<template #label>
|
||||
<apiStatus :status="scenario.status" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(ApiScenarioStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('common.tag')" class="mb-[16px]">
|
||||
<MsTagsInput v-model:model-value="scenario.tags" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('common.desc')" class="mb-[16px]">
|
||||
<a-textarea
|
||||
v-model:model-value="scenario.description"
|
||||
:max-length="500"
|
||||
:placeholder="t('apiScenario.descPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="props.isEdit">
|
||||
<a-form-item field="createUser" :label="t('apiScenario.table.columns.createUser')" class="mb-[16px]">
|
||||
<a-input :model-value="(scenario as ScenarioDetail).createUser" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item field="createTime" :label="t('apiScenario.table.columns.createTime')" class="mb-[16px]">
|
||||
<a-input :model-value="dayjs((scenario as ScenarioDetail).createTime).format('YYYY-MM-DD HH:mm:ss')" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item field="updateTime" :label="t('apiScenario.table.columns.updateTime')" class="mb-[16px]">
|
||||
<a-input :model-value="dayjs((scenario as ScenarioDetail).updateTime).format('YYYY-MM-DD HH:mm:ss')" disabled />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ApiScenarioStatus } from '@/enums/apiEnum';
|
||||
|
||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||
|
||||
const props = defineProps<{
|
||||
scenario: ScenarioDetail;
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
isEdit?: boolean;
|
||||
}>();
|
||||
const scenario = defineModel<ScenarioDetail | Scenario>('scenario', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const descriptions = computed<Description[]>(() => [
|
||||
{
|
||||
label: t('apiScenario.belongModule'),
|
||||
value: props.scenario.modulePath,
|
||||
},
|
||||
{
|
||||
label: t('apiScenario.table.columns.createUser'),
|
||||
value: props.scenario.createUser,
|
||||
},
|
||||
{
|
||||
label: t('apiScenario.table.columns.createTime'),
|
||||
value: dayjs(props.scenario.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
label: t('apiScenario.table.columns.updateTime'),
|
||||
value: dayjs(props.scenario.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
const createFormRef = ref<FormInstance>();
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
defineExpose({
|
||||
createFormRef,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -127,6 +127,15 @@
|
|||
/>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_SCENARIO:READ+UPDATE']"
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
@click="openScenarioTab(record)"
|
||||
>
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<a-divider v-permission="['PROJECT_API_SCENARIO:READ+UPDATE']" direction="vertical" :margin="8"></a-divider>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
|
||||
type="text"
|
||||
|
@ -522,7 +531,7 @@
|
|||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: 180,
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||
|
|
|
@ -61,62 +61,7 @@
|
|||
<div class="p-[16px]">
|
||||
<!-- TODO:第一版没有模板 -->
|
||||
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
|
||||
<a-form ref="createFormRef" :model="scenario" layout="vertical">
|
||||
<a-form-item
|
||||
field="name"
|
||||
:label="t('apiScenario.name')"
|
||||
class="mb-[16px]"
|
||||
:rules="[{ required: true, message: t('apiScenario.nameRequired') }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="scenario.name"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.namePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.belongModule')" class="mb-[16px]">
|
||||
<a-tree-select
|
||||
v-model:modelValue="scenario.moduleId"
|
||||
:data="props.moduleTree"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
height: 200,
|
||||
threshold: 200,
|
||||
},
|
||||
}"
|
||||
allow-search
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.scenarioLevel')">
|
||||
<a-select v-model:model-value="scenario.priority" :placeholder="t('common.pleaseSelect')">
|
||||
<template #label>
|
||||
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="scenario.priority" /></span>
|
||||
</template>
|
||||
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||
<caseLevel :case-level="item.label as CaseLevel" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.status')" class="mb-[16px]">
|
||||
<a-select
|
||||
v-model:model-value="scenario.status"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
>
|
||||
<template #label>
|
||||
<apiStatus :status="scenario.status" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(ApiScenarioStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('common.tag')" class="mb-[16px]">
|
||||
<MsTagsInput v-model:model-value="scenario.tags" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<baseInfo ref="baseInfoRef" :scenario="scenario as Scenario" :module-tree="props.moduleTree" />
|
||||
<!-- TODO:第一版先不做依赖 -->
|
||||
<!-- <div class="mb-[8px] flex items-center">
|
||||
<div class="text-[var(--color-text-2)]">
|
||||
|
@ -166,21 +111,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
import baseInfo from '../components/baseInfo.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||
import { ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
// 组成部分异步导入
|
||||
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
||||
|
@ -204,10 +142,10 @@
|
|||
});
|
||||
|
||||
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
|
||||
const createFormRef = ref<FormInstance>();
|
||||
const baseInfoRef = ref<InstanceType<typeof baseInfo>>();
|
||||
|
||||
function validScenarioForm(cb: () => Promise<void>) {
|
||||
createFormRef.value?.validate(async (errors) => {
|
||||
baseInfoRef.value?.createFormRef?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
splitBoxRef.value?.expand();
|
||||
} else {
|
||||
|
|
|
@ -3,31 +3,16 @@
|
|||
<div class="px-[24px] pt-[16px]">
|
||||
<MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description" class="!py-[8px]">
|
||||
<template #titleAppend>
|
||||
<apiStatus :status="scenario.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
|
||||
:loading="followLoading"
|
||||
:type="scenario.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="`${scenario.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
|
||||
:size="14"
|
||||
class="cursor-pointer"
|
||||
:size="16"
|
||||
@click="toggleFollowReview"
|
||||
/>
|
||||
{{ t(scenario.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>
|
||||
<MsIcon type="icon-icon_share1" class="cursor-pointer text-[var(--color-text-4)]" :size="16" @click="share" />
|
||||
<apiStatus :status="scenario.status" size="small" />
|
||||
</template>
|
||||
<template #priority="{ value }">
|
||||
<caseLevel :case-level="value as CaseLevel" />
|
||||
|
@ -39,9 +24,15 @@
|
|||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.BASE_INFO"
|
||||
:title="t('apiScenario.baseInfo')"
|
||||
class="scenario-detail-tab-pane"
|
||||
class="scenario-detail-tab-pane base-info-pane"
|
||||
>
|
||||
<baseInfo :scenario="scenario as ScenarioDetail" />
|
||||
<baseInfo
|
||||
ref="baseInfoRef"
|
||||
is-edit
|
||||
:scenario="scenario as ScenarioDetail"
|
||||
:module-tree="props.moduleTree"
|
||||
class="w-[30%]"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.STEP"
|
||||
|
@ -140,6 +131,7 @@
|
|||
import { followScenario } from '@/api/modules/api-test/scenario';
|
||||
|
||||
import { ApiScenarioDebugRequest, Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ScenarioDetailComposition } from '@/enums/apiEnum';
|
||||
|
||||
// 组成部分异步导入
|
||||
|
@ -152,6 +144,9 @@
|
|||
// const quote = defineAsyncComponent(() => import('../components/quote.vue'));
|
||||
const setting = defineAsyncComponent(() => import('../components/setting.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void;
|
||||
(e: 'updateFollow'): void;
|
||||
|
@ -208,6 +203,21 @@
|
|||
}
|
||||
|
||||
const activeKey = ref<ScenarioDetailComposition>(ScenarioDetailComposition.STEP);
|
||||
|
||||
const baseInfoRef = ref<InstanceType<typeof baseInfo>>();
|
||||
function validScenarioForm(cb: () => Promise<void>) {
|
||||
baseInfoRef.value?.createFormRef?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
activeKey.value = ScenarioDetailComposition.BASE_INFO;
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validScenarioForm,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -227,5 +237,9 @@
|
|||
.scenario-detail-tab-pane {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.base-info-pane {
|
||||
@apply h-full overflow-auto;
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
</MsEditableTab>
|
||||
<div v-show="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]">
|
||||
<environmentSelect v-model:current-env-config="currentEnvConfig" />
|
||||
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
<executeButton
|
||||
ref="executeButtonRef"
|
||||
:execute-loading="activeScenarioTab.executeLoading"
|
||||
@execute="handleExecute"
|
||||
@stop-debug="handleStopExecute"
|
||||
/>
|
||||
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider class="!my-0" />
|
||||
|
@ -75,7 +75,12 @@
|
|||
></create>
|
||||
</div>
|
||||
<div v-else class="pageWrap">
|
||||
<detail v-model:scenario="activeScenarioTab" @batch-debug="realExecute($event, false)"></detail>
|
||||
<detail
|
||||
ref="detailRef"
|
||||
v-model:scenario="activeScenarioTab"
|
||||
:module-tree="folderTree"
|
||||
@batch-debug="realExecute($event, false)"
|
||||
></detail>
|
||||
</div>
|
||||
</MsCard>
|
||||
</template>
|
||||
|
@ -440,6 +445,7 @@
|
|||
}
|
||||
|
||||
const createRef = ref<InstanceType<typeof create>>();
|
||||
const detailRef = ref<InstanceType<typeof detail>>();
|
||||
const saveLoading = ref(false);
|
||||
|
||||
async function realSaveScenario() {
|
||||
|
@ -500,7 +506,7 @@
|
|||
if (activeScenarioTab.value.isNew) {
|
||||
createRef.value?.validScenarioForm(realSaveScenario);
|
||||
} else {
|
||||
realSaveScenario();
|
||||
detailRef.value?.validScenarioForm(realSaveScenario);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ export default {
|
|||
// 批量操作文案
|
||||
'api_scenario.batch_operation.success': 'Success {opt} to {name}',
|
||||
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios',
|
||||
'apiScenario.descPlaceholder': 'Please describe the scenario',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name',
|
||||
'apiScenario.executeHistory.num': 'Number',
|
||||
|
|
|
@ -69,6 +69,7 @@ export default {
|
|||
'apiScenario.belongModule': '所属模块',
|
||||
'apiScenario.level': '场景等级',
|
||||
'apiScenario.status': '场景状态',
|
||||
'apiScenario.descPlaceholder': '请对该场景进行描述',
|
||||
'apiScenario.addStep': '添加步骤',
|
||||
'apiScenario.requestScenario': '请求/场景',
|
||||
'apiScenario.importSystemApi': '导入系统请求',
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
</template>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
<detail v-else v-model:scenario="activeApiTab"></detail>
|
||||
<!-- <detail v-else v-model:scenario="activeApiTab"></detail> -->
|
||||
</MsCard>
|
||||
</template>
|
||||
|
||||
|
@ -57,7 +57,7 @@
|
|||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import detail from './detail/index.vue';
|
||||
// import detail from './detail/index.vue';
|
||||
import RecycleTable from '@/views/api-test/scenario/recycle/recycleTable.vue';
|
||||
import recycleTree from '@/views/api-test/scenario/recycle/recycleTree.vue';
|
||||
|
||||
|
|
Loading…
Reference in New Issue