fix(接口测试): 定义和场景的列表操作及详情新增编辑按钮&调整样式

This commit is contained in:
teukkk 2024-03-28 23:51:39 +08:00 committed by Craftsman
parent 655ea48f14
commit 6bccc40e9c
18 changed files with 324 additions and 205 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="ms-detail-card"> <div class="ms-detail-card">
<div class="ms-detail-card-title flex items-center justify-between"> <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)"> <a-tooltip :content="t(props.title)">
<div class="one-line-text flex-1 font-medium text-[var(--color-text-1)]"> <div class="one-line-text flex-1 font-medium text-[var(--color-text-1)]">
{{ t(props.title) }} {{ t(props.title) }}

View File

@ -1592,6 +1592,8 @@
}); });
defineExpose({ defineExpose({
execute,
isPriorityLocalExec,
makeRequestParams, makeRequestParams,
changeVerticalExpand, changeVerticalExpand,
}); });

View File

@ -234,7 +234,6 @@
* 响应状态码对应颜色 * 响应状态码对应颜色
*/ */
const statusCodeColor = computed(() => { const statusCodeColor = computed(() => {
debugger;
if (activeStepDetailCopy.value?.content) { if (activeStepDetailCopy.value?.content) {
const code = Number(activeStepDetailCopy.value?.content?.responseResult.responseCode); const code = Number(activeStepDetailCopy.value?.content?.responseResult.responseCode);
if (code >= 200 && code < 300) { if (code >= 200 && code < 300) {

View File

@ -129,6 +129,15 @@
<apiStatus v-else :status="record.status" size="small" /> <apiStatus v-else :status="record.status" size="small" />
</template> </template>
<template #action="{ record }"> <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 <MsButton
v-permission="['PROJECT_API_DEFINITION:READ+EXECUTE']" v-permission="['PROJECT_API_DEFINITION:READ+EXECUTE']"
type="text" type="text"
@ -343,6 +352,7 @@
(e: 'openCopyApiTab', record: ApiDefinitionDetail): void; (e: 'openCopyApiTab', record: ApiDefinitionDetail): void;
(e: 'addApiTab'): void; (e: 'addApiTab'): void;
(e: 'import'): void; (e: 'import'): void;
(e: 'openEditApiTab', record: ApiDefinitionDetail, isCopy: boolean, isExecute: boolean, isEdit: boolean): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -445,7 +455,7 @@
slotName: 'action', slotName: 'action',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: hasOperationPermission.value ? 150 : 50, width: hasOperationPermission.value ? 200 : 50,
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
@ -872,6 +882,10 @@
emit('openApiTab', record, true); emit('openApiTab', record, true);
} }
function editDefinition(record: ApiDefinitionDetail) {
emit('openEditApiTab', record, false, false, true);
}
// //
async function handleTableDragSort(params: DragSortParams) { async function handleTableDragSort(params: DragSortParams) {
try { try {

View File

@ -10,6 +10,7 @@
@open-copy-api-tab="openApiTab($event, true)" @open-copy-api-tab="openApiTab($event, true)"
@add-api-tab="addApiTab" @add-api-tab="addApiTab"
@import="emit('import')" @import="emit('import')"
@open-edit-api-tab="openApiTab"
/> />
</div> </div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden"> <div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
@ -20,6 +21,34 @@
class="ms-api-tab-nav" class="ms-api-tab-nav"
@change="changeDefinitionActiveKey" @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 <a-tab-pane
v-if="!activeApiTab.isNew" v-if="!activeApiTab.isNew"
key="preview" key="preview"
@ -36,6 +65,7 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane"> <a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane">
<requestComposition <requestComposition
ref="requestCompositionRef"
v-model:detail-loading="loading" v-model:detail-loading="loading"
v-model:request="activeApiTab" v-model:request="activeApiTab"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
@ -77,7 +107,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
// import MsButton from '@/components/pure/ms-button/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import caseTable from '../case/caseTable.vue'; import caseTable from '../case/caseTable.vue';
@ -88,6 +117,7 @@
import { import {
addDefinition, addDefinition,
debugDefinition, debugDefinition,
deleteDefinition,
getDefinitionDetail, getDefinitionDetail,
getTransferOptions, getTransferOptions,
transferFile, transferFile,
@ -95,6 +125,7 @@
uploadTempFile, uploadTempFile,
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common'; import { ProtocolItem } from '@/models/apiTest/common';
@ -126,6 +157,7 @@
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'deleteApi', id: string): void;
(e: 'import'): void; (e: 'import'): void;
}>(); }>();
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
@ -134,6 +166,7 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal();
const apiTabs = defineModel<RequestParam[]>('apiTabs', { const apiTabs = defineModel<RequestParam[]>('apiTabs', {
required: true, required: true,
@ -254,7 +287,12 @@
); );
const loading = ref(false); 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( const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id) (e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
); );
@ -262,6 +300,7 @@
// tabtab // tabtab
activeApiTab.value = { activeApiTab.value = {
...(apiTabs.value[isLoadedTabIndex] as RequestParam), ...(apiTabs.value[isLoadedTabIndex] as RequestParam),
definitionActiveKey: isCopy || isExecute || isEdit ? 'definition' : 'preview',
isExecute, isExecute,
mode: isExecute ? 'debug' : 'definition', mode: isExecute ? 'debug' : 'definition',
}; };
@ -289,7 +328,7 @@
id: isCopy ? new Date().getTime() : res.id, id: isCopy ? new Date().getTime() : res.id,
isExecute, isExecute,
mode: isExecute ? 'debug' : 'definition', mode: isExecute ? 'debug' : 'definition',
definitionActiveKey: isCopy || isExecute ? 'definition' : 'preview', definitionActiveKey: isCopy || isExecute || isEdit ? 'definition' : 'preview',
...parseRequestBodyResult, ...parseRequestBodyResult,
}); });
nextTick(() => { 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({ defineExpose({
openApiTab, openApiTab,
addApiTab, addApiTab,
@ -328,9 +406,15 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
:deep(.ms-api-tab-nav) { :deep(.ms-api-tab-nav) {
@apply h-full; @apply h-full;
.arco-tabs-nav-tab { .arco-tabs-nav {
border-bottom: 1px solid var(--color-text-n8); border-bottom: 1px solid var(--color-text-n8);
} }
.arco-tabs-content { .arco-tabs-content {

View File

@ -7,32 +7,17 @@
:simple-show-count="4" :simple-show-count="4"
> >
<template #titleAppend> <template #titleAppend>
<apiStatus :status="previewDetail.status" size="small" /> <MsIcon
</template>
<template #titleRight>
<a-button
v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']" v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']"
type="outline"
:loading="followLoading" :loading="followLoading"
size="mini" :type="previewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="arco-btn-outline--secondary mr-[4px] !bg-transparent" :class="`${previewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
class="cursor-pointer"
:size="16"
@click="toggleFollowReview" @click="toggleFollowReview"
> />
<div class="flex items-center gap-[4px]"> <MsIcon type="icon-icon_share1" class="cursor-pointer text-[var(--color-text-4)]" :size="16" @click="share" />
<MsIcon <apiStatus :status="previewDetail.status" size="small" />
: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>
<template #type="{ value }"> <template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag /> <apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
@ -171,6 +156,7 @@
followLoading.value = true; followLoading.value = true;
await toggleFollowDefinition(previewDetail.value.id); await toggleFollowDefinition(previewDetail.value.id);
Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess')); Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
previewDetail.value.follow = !previewDetail.value.follow;
emit('updateFollow'); emit('updateFollow');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -11,39 +11,44 @@
@stop-debug="stopDebug" @stop-debug="stopDebug"
@execute="handleExecute" @execute="handleExecute"
/> />
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect"> <a-dropdown-button v-if="!props.isDrawer" type="outline" @click="editCase">
<a-button v-if="!props.isDrawer" type="outline">{{ t('common.operation') }}</a-button> {{ t('common.edit') }}
<template #icon>
<icon-down />
</template>
<template #content> <template #content>
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="edit">
<MsIcon type="icon-icon_edit_outlined" />
{{ 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" />
<a-doption <a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
value="delete" value="delete"
class="error-6 text-[rgb(var(--danger-6))]" class="error-6 text-[rgb(var(--danger-6))]"
@click="handleDelete"
> >
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" /> <MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }} {{ t('common.delete') }}
</a-doption> </a-doption>
</template> </template>
</a-dropdown> </a-dropdown-button>
</div> </div>
</template> </template>
<a-tab-pane key="detail" :title="t('case.detail')" class="px-[18px] py-[16px]"> <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]"> <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 }"> <template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag /> <apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template> </template>
@ -159,6 +164,7 @@
followLoading.value = true; followLoading.value = true;
await toggleFollowCase(caseDetail.value.id); await toggleFollowCase(caseDetail.value.id);
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess')); Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
caseDetail.value.follow = !caseDetail.value.follow;
emit('updateFollow'); emit('updateFollow');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // 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 protocols = inject<Ref<ProtocolItem[]>>('protocols');
const currentEnvConfigByInject = inject<Ref<EnvConfig>>('currentEnvConfig'); const currentEnvConfigByInject = inject<Ref<EnvConfig>>('currentEnvConfig');

View File

@ -30,7 +30,7 @@
> >
<MsIcon <MsIcon
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'" :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') }} {{ t('common.fork') }}
</MsButton> </MsButton>

View File

@ -167,6 +167,19 @@
</div> </div>
</template> </template>
<template #operation="{ record }"> <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 <MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']" v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
type="text" type="text"
@ -335,7 +348,6 @@
batchExecuteCase, batchExecuteCase,
deleteCase, deleteCase,
dragSort, dragSort,
executeCase,
getCaseDetail, getCaseDetail,
getCasePage, getCasePage,
updateCasePriority, updateCasePriority,
@ -516,7 +528,7 @@
slotName: 'operation', slotName: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: hasOperationPermission.value ? 150 : 50, width: hasOperationPermission.value ? 200 : 50,
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, { 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() { function cancelBatchEdit() {
showBatchEditModal.value = false; showBatchEditModal.value = false;
batchFormRef.value?.resetFields(); batchFormRef.value?.resetFields();
@ -943,6 +945,11 @@
loadCaseList(); loadCaseList();
} }
async function editCase(record: ApiCaseDetail) {
await getCaseDetailInfo(record.id);
createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, caseDetail.value as RequestParam);
}
// api // api
function loadCase(id: string) { function loadCase(id: string) {
getCaseDetailInfo(id); getCaseDetailInfo(id);

View File

@ -40,6 +40,7 @@
:protocol="props.protocol" :protocol="props.protocol"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
@import="emit('import')" @import="emit('import')"
@delete-api="(id) => handleDeleteApiFromModuleTree(id)"
/> />
<apiCase <apiCase
v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'" v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"

View File

@ -1,40 +1,110 @@
<template> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance } from '@arco-design/web-vue';
import dayjs from 'dayjs'; 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 { 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<{ const props = defineProps<{
scenario: ScenarioDetail; moduleTree: ModuleTreeNode[]; //
isEdit?: boolean;
}>(); }>();
const scenario = defineModel<ScenarioDetail | Scenario>('scenario', {
required: true,
});
const { t } = useI18n(); const { t } = useI18n();
const descriptions = computed<Description[]>(() => [ const createFormRef = ref<FormInstance>();
{
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>
<style lang="less" scoped></style> defineExpose({
createFormRef,
});
</script>

View File

@ -127,6 +127,15 @@
/> />
</template> </template>
<template #operation="{ record }"> <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 <MsButton
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']" v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
type="text" type="text"
@ -522,7 +531,7 @@
slotName: 'operation', slotName: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: 180, width: 200,
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(

View File

@ -61,62 +61,7 @@
<div class="p-[16px]"> <div class="p-[16px]">
<!-- TODO:第一版没有模板 --> <!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> --> <!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="createFormRef" :model="scenario" layout="vertical"> <baseInfo ref="baseInfoRef" :scenario="scenario as Scenario" :module-tree="props.moduleTree" />
<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>
<!-- TODO:第一版先不做依赖 --> <!-- TODO:第一版先不做依赖 -->
<!-- <div class="mb-[8px] flex items-center"> <!-- <div class="mb-[8px] flex items-center">
<div class="text-[var(--color-text-2)]"> <div class="text-[var(--color-text-2)]">
@ -166,21 +111,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance } from '@arco-design/web-vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import baseInfo from '../components/baseInfo.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 { useI18n } from '@/hooks/useI18n';
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario'; import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum'; import { ScenarioCreateComposition } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
// //
const step = defineAsyncComponent(() => import('../components/step/index.vue')); const step = defineAsyncComponent(() => import('../components/step/index.vue'));
@ -204,10 +142,10 @@
}); });
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>(); const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const createFormRef = ref<FormInstance>(); const baseInfoRef = ref<InstanceType<typeof baseInfo>>();
function validScenarioForm(cb: () => Promise<void>) { function validScenarioForm(cb: () => Promise<void>) {
createFormRef.value?.validate(async (errors) => { baseInfoRef.value?.createFormRef?.validate(async (errors) => {
if (errors) { if (errors) {
splitBoxRef.value?.expand(); splitBoxRef.value?.expand();
} else { } else {

View File

@ -3,31 +3,16 @@
<div class="px-[24px] pt-[16px]"> <div class="px-[24px] pt-[16px]">
<MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description" class="!py-[8px]"> <MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description" class="!py-[8px]">
<template #titleAppend> <template #titleAppend>
<apiStatus :status="scenario.status" size="small" /> <MsIcon
</template>
<template #titleRight>
<a-button
type="outline"
:loading="followLoading" :loading="followLoading"
size="mini" :type="scenario.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="arco-btn-outline--secondary mr-[4px] !bg-transparent" :class="`${scenario.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
class="cursor-pointer"
:size="16"
@click="toggleFollowReview" @click="toggleFollowReview"
> />
<div class="flex items-center gap-[4px]"> <MsIcon type="icon-icon_share1" class="cursor-pointer text-[var(--color-text-4)]" :size="16" @click="share" />
<MsIcon <apiStatus :status="scenario.status" size="small" />
: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"
/>
{{ 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>
</template> </template>
<template #priority="{ value }"> <template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" /> <caseLevel :case-level="value as CaseLevel" />
@ -39,9 +24,15 @@
<a-tab-pane <a-tab-pane
:key="ScenarioDetailComposition.BASE_INFO" :key="ScenarioDetailComposition.BASE_INFO"
:title="t('apiScenario.baseInfo')" :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>
<a-tab-pane <a-tab-pane
:key="ScenarioDetailComposition.STEP" :key="ScenarioDetailComposition.STEP"
@ -140,6 +131,7 @@
import { followScenario } from '@/api/modules/api-test/scenario'; import { followScenario } from '@/api/modules/api-test/scenario';
import { ApiScenarioDebugRequest, Scenario, ScenarioDetail } from '@/models/apiTest/scenario'; import { ApiScenarioDebugRequest, Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { ScenarioDetailComposition } from '@/enums/apiEnum'; import { ScenarioDetailComposition } from '@/enums/apiEnum';
// //
@ -152,6 +144,9 @@
// const quote = defineAsyncComponent(() => import('../components/quote.vue')); // const quote = defineAsyncComponent(() => import('../components/quote.vue'));
const setting = defineAsyncComponent(() => import('../components/setting.vue')); const setting = defineAsyncComponent(() => import('../components/setting.vue'));
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
}>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void; (e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void;
(e: 'updateFollow'): void; (e: 'updateFollow'): void;
@ -208,6 +203,21 @@
} }
const activeKey = ref<ScenarioDetailComposition>(ScenarioDetailComposition.STEP); 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -227,5 +237,9 @@
.scenario-detail-tab-pane { .scenario-detail-tab-pane {
padding: 8px 16px; padding: 8px 16px;
} }
.base-info-pane {
@apply h-full overflow-auto;
.ms-scroll-bar();
}
} }
</style> </style>

View File

@ -17,15 +17,15 @@
</MsEditableTab> </MsEditableTab>
<div v-show="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]"> <div v-show="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]">
<environmentSelect v-model:current-env-config="currentEnvConfig" /> <environmentSelect v-model:current-env-config="currentEnvConfig" />
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
{{ t('common.save') }}
</a-button>
<executeButton <executeButton
ref="executeButtonRef" ref="executeButtonRef"
:execute-loading="activeScenarioTab.executeLoading" :execute-loading="activeScenarioTab.executeLoading"
@execute="handleExecute" @execute="handleExecute"
@stop-debug="handleStopExecute" @stop-debug="handleStopExecute"
/> />
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
{{ t('common.save') }}
</a-button>
</div> </div>
</div> </div>
<a-divider class="!my-0" /> <a-divider class="!my-0" />
@ -75,7 +75,12 @@
></create> ></create>
</div> </div>
<div v-else class="pageWrap"> <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> </div>
</MsCard> </MsCard>
</template> </template>
@ -440,6 +445,7 @@
} }
const createRef = ref<InstanceType<typeof create>>(); const createRef = ref<InstanceType<typeof create>>();
const detailRef = ref<InstanceType<typeof detail>>();
const saveLoading = ref(false); const saveLoading = ref(false);
async function realSaveScenario() { async function realSaveScenario() {
@ -500,7 +506,7 @@
if (activeScenarioTab.value.isNew) { if (activeScenarioTab.value.isNew) {
createRef.value?.validScenarioForm(realSaveScenario); createRef.value?.validScenarioForm(realSaveScenario);
} else { } else {
realSaveScenario(); detailRef.value?.validScenarioForm(realSaveScenario);
} }
} }

View File

@ -51,6 +51,7 @@ export default {
// 批量操作文案 // 批量操作文案
'api_scenario.batch_operation.success': 'Success {opt} to {name}', 'api_scenario.batch_operation.success': 'Success {opt} to {name}',
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios', '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.searchPlaceholder': 'Search by ID or name',
'apiScenario.executeHistory.num': 'Number', 'apiScenario.executeHistory.num': 'Number',

View File

@ -69,6 +69,7 @@ export default {
'apiScenario.belongModule': '所属模块', 'apiScenario.belongModule': '所属模块',
'apiScenario.level': '场景等级', 'apiScenario.level': '场景等级',
'apiScenario.status': '场景状态', 'apiScenario.status': '场景状态',
'apiScenario.descPlaceholder': '请对该场景进行描述',
'apiScenario.addStep': '添加步骤', 'apiScenario.addStep': '添加步骤',
'apiScenario.requestScenario': '请求/场景', 'apiScenario.requestScenario': '请求/场景',
'apiScenario.importSystemApi': '导入系统请求', 'apiScenario.importSystemApi': '导入系统请求',

View File

@ -43,7 +43,7 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
<detail v-else v-model:scenario="activeApiTab"></detail> <!-- <detail v-else v-model:scenario="activeApiTab"></detail> -->
</MsCard> </MsCard>
</template> </template>
@ -57,7 +57,7 @@
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue'; import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/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 RecycleTable from '@/views/api-test/scenario/recycle/recycleTable.vue';
import recycleTree from '@/views/api-test/scenario/recycle/recycleTree.vue'; import recycleTree from '@/views/api-test/scenario/recycle/recycleTree.vue';