feat(接口场景): 场景步骤 2%&部分 bug 解决

This commit is contained in:
baiqi 2024-03-15 14:02:21 +08:00 committed by Craftsman
parent 868a1a38cb
commit a158207ce5
17 changed files with 777 additions and 60 deletions

View File

@ -1,13 +1,12 @@
<template>
<a-spin class="!block h-full" :loading="props.loading" :size="28">
<a-spin class="!block" :loading="props.loading" :size="28">
<div
ref="fullRef"
:class="[
'ms-card',
'relative',
'h-full',
props.isFullscreen || isFullScreen ? 'ms-card--fullScreen' : '',
props.autoHeight ? '' : 'min-h-[500px]',
props.autoHeight ? '' : 'h-full min-h-[500px]',
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
props.noBottomRadius ? 'ms-card--noBottomRadius' : '',
!props.hideFooter && !props.simple ? 'pb-[80px]' : '',

View File

@ -1,5 +1,5 @@
import { ConditionType } from '@/models/apiTest/common';
import { RequestBodyFormat, RequestConditionProcessor } from '@/enums/apiEnum';
import { RequestBodyFormat, RequestConditionProcessor, ScenarioStepType } from '@/enums/apiEnum';
// 条件操作类型
export type ConditionTypeNameMap = Record<ConditionType, string>;

View File

@ -236,3 +236,34 @@ export enum ScenarioDetailComposition {
DEPENDENCY = 'DEPENDENCY',
QUOTE = 'QUOTE',
}
// 场景执行状态
export enum ScenarioExecuteStatus {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
STOP = 'STOP',
}
// 场景步骤类型
export enum ScenarioStepType {
QUOTE_API = 'QUOTE_API',
COPY_API = 'COPY_API',
QUOTE_CASE = 'QUOTE_CASE',
COPY_CASE = 'COPY_CASE',
QUOTE_SCENARIO = 'QUOTE_SCENARIO',
COPY_SCENARIO = 'COPY_SCENARIO',
WAIT_TIME = 'WAIT_TIME',
LOOP_CONTROL = 'LOOP_CONTROL',
CONDITION_CONTROL = 'CONDITION_CONTROL',
ONLY_ONCE_CONTROL = 'ONLY_ONCE_CONTROL',
SCRIPT_OPERATION = 'SCRIPT_OPERATION',
CUSTOM_API = 'CUSTOM_API',
}
// 场景添加步骤操作类型
export enum ScenarioAddStepActionType {
IMPORT_SYSTEM_API = 'IMPORT_SYSTEM_API',
CUSTOM_API = 'CUSTOM_API',
LOOP_CONTROL = 'LOOP_CONTROL',
CONDITION_CONTROL = 'CONDITION_CONTROL',
ONLY_ONCE_CONTROL = 'ONLY_ONCE_CONTROL',
SCRIPT_OPERATION = 'SCRIPT_OPERATION',
WAIT_TIME = 'WAIT_TIME',
}

View File

@ -133,4 +133,9 @@ export default {
'common.share': 'Share',
'common.notRemind': `Don't remind again`,
'common.status': 'Status',
'common.batchEnable': 'Batch enable',
'common.batchDisable': 'Batch disable',
'common.batchDelete': 'Batch delete',
'common.batchDebug': 'Batch debug',
'common.quote': 'Quote',
};

View File

@ -136,4 +136,9 @@ export default {
'common.share': '分享',
'common.notRemind': '不再提醒',
'common.status': '状态',
'common.batchEnable': '批量启用',
'common.batchDisable': '批量禁用',
'common.batchDelete': '批量删除',
'common.batchDebug': '批量调试',
'common.quote': '引用',
};

View File

@ -167,15 +167,7 @@
isExpandAll?: boolean; //
activeNodeId?: string | number; // id
}>();
const emit = defineEmits([
'init',
'clickApiNode',
'newApi',
'import',
'renameFinish',
'deleteFinish',
'updateApiNode',
]);
const emit = defineEmits(['init', 'clickApiNode', 'newApi', 'import', 'updateApiNode', 'deleteFinish']);
const appStore = useAppStore();
const { t } = useI18n();
@ -456,7 +448,7 @@
}
async function handleRenameFinish(newName: string, id: string) {
emit('renameFinish', newName, id);
emit('updateApiNode', { name: newName, id });
await initModules();
initModuleCount();
}

View File

@ -10,7 +10,7 @@
@new-api="addDebugTab"
@click-api-node="openApiTab"
@import="importDrawerVisible = true"
@rename-finish="handleRenameFinish"
@update-api-node="handleApiUpdateFromModuleTree"
@delete-finish="handleDeleteFinish"
/>
</template>
@ -121,11 +121,9 @@
import { parseCurlScript } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { ExecuteBody, RequestTaskResult } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestContentTypeEnum,
RequestMethods,
@ -295,34 +293,49 @@
});
}
function handleRenameFinish(name: string, id: string) {
debugTabs.value = debugTabs.value.map((tab) => {
if (tab.id === id) {
tab.label = name;
tab.name = name;
/**
* 同步模块树的接口信息更新操作
*/
function handleApiUpdateFromModuleTree(newInfo: { id: string; name: string; moduleId?: string; [key: string]: any }) {
debugTabs.value = debugTabs.value.map((item) => {
if (item.id === newInfo.id) {
item.label = newInfo.name;
item.name = newInfo.name;
if (newInfo.moduleId) {
item.moduleId = newInfo.moduleId;
}
}
return tab;
return item;
});
if (activeDebug.value.id === newInfo.id) {
activeDebug.value.label = newInfo.name;
activeDebug.value.name = newInfo.name;
if (newInfo.moduleId) {
activeDebug.value.moduleId = newInfo.moduleId;
}
}
}
/**
* 同步模块树的接口信息删除操作
* @param id 接口 id
* @param isModule 是否是删除模块
*/
function handleDeleteFinish(node: ModuleTreeNode) {
let index;
if (node.type === 'API') {
//
index = debugTabs.value.findIndex((tab) => tab.id === node.id);
} else {
//
index = debugTabs.value.findIndex((tab) => tab.moduleId === node.id);
}
if (index > -1) {
debugTabs.value.splice(index, 1);
if (activeDebug.value.id === node.id) {
// tabtab
if (debugTabs.value.length > 0) {
if (node.type === 'MODULE') {
//
debugTabs.value = debugTabs.value.filter((item) => {
if (activeDebug.value.id === item.id) {
// tab, tab
[activeDebug.value] = debugTabs.value;
} else {
addDebugTab();
}
return item.moduleId !== node.id || item.id === 'all';
});
} else {
// api
debugTabs.value = debugTabs.value.filter((item) => item.id !== node.id);
if (activeDebug.value.id === node.id) {
[activeDebug.value] = debugTabs.value;
}
}
}

View File

@ -79,13 +79,12 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ExecuteBody, ProtocolItem, RequestTaskResult } from '@/models/apiTest/common';
import { ProtocolItem } from '@/models/apiTest/common';
import { ApiDefinitionDetail } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestDefinitionStatus,
RequestMethods,

View File

@ -0,0 +1,48 @@
<template>
<MsTag :self-style="status.style" :size="props.size"> {{ status.text }}</MsTag>
</template>
<script setup lang="ts">
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioExecuteStatus } from '@/enums/apiEnum';
const props = defineProps<{
status: ScenarioExecuteStatus;
size?: Size;
}>();
const { t } = useI18n();
const statusMap = {
[ScenarioExecuteStatus.STOP]: {
bgColor: 'rgb(var(--link-2))',
color: 'rgb(var(--link-6))',
text: 'common.stop',
},
[ScenarioExecuteStatus.FAILED]: {
bgColor: 'rgb(var(--danger-2))',
color: 'rgb(var(--danger-6))',
text: 'common.failed',
},
[ScenarioExecuteStatus.SUCCESS]: {
bgColor: 'rgb(var(--success-2))',
color: 'rgb(var(--success-6))',
text: 'common.success',
},
};
const status = computed(() => {
const config = statusMap[props.status];
return {
style: {
backgroundColor: config?.bgColor,
color: config?.color,
},
text: t(config?.text),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,101 @@
<template>
<MsDrawer v-model:visible="visible" :title="t('apiScenario.importSystemApi')" :width="1200">
<div class="flex h-full flex-col overflow-hidden">
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="api" :title="t('apiScenario.api')" />
<a-tab-pane key="case" :title="t('apiScenario.case')" />
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
</a-tabs>
<div class="flex-1">
<div class="flex">
<div class="p-[16px]"></div>
</div>
</div>
</div>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<div class="second-text">{{ t('apiScenario.sumSelected') }}</div>
<div class="main-text">{{ totalSelected }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.api') }}</div>
<div class="main-text">{{ selectedApis.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.case') }}</div>
<div class="main-text">{{ selectedCases.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<div class="second-text">{{ t('apiScenario.scenario') }}</div>
<div class="main-text">{{ selectedScenarios.length }}</div>
<a-divider direction="vertical" :margin="4"></a-divider>
<MsButton v-show="totalSelected > 0" type="text" class="!mr-0 ml-[4px]" @click="clearAll">
{{ t('common.clear') }}
</MsButton>
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="handleCopy">{{ t('common.copy') }}</a-button>
<a-button type="primary" @click="handleQuote">{{ t('common.quote') }}</a-button>
</div>
</div>
</template>
</MsDrawer>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const emit = defineEmits<{
(e: 'copy', data: any[]): void;
(e: 'quote', data: any[]): void;
}>();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
required: true,
});
const activeKey = ref('api');
const selectedApis = ref<any[]>([]);
const selectedCases = ref<any[]>([]);
const selectedScenarios = ref<any[]>([]);
const totalSelected = computed(() => {
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
});
function clearAll() {
selectedApis.value = [];
selectedCases.value = [];
selectedScenarios.value = [];
}
function handleCancel() {
clearAll();
visible.value = false;
}
function handleCopy() {
emit('copy', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
function handleQuote() {
emit('quote', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
handleCancel();
}
</script>
<style lang="less" scoped>
.second-text {
color: var(--color-text-2);
}
.main-text {
color: rgb(var(--primary-5));
}
.arco-tabs-content {
@apply hidden;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div
class="rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{
borderColor: status.color,
color: status.color,
}"
>
{{ status.label }}
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{
status: ScenarioStepType;
}>();
const { t } = useI18n();
//
const scenarioStepMap = {
[ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROL]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.CONDITION_CONTROL]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONLY_ONCE_CONTROL]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
};
const status = computed(() => {
const config = scenarioStepMap[props.status];
return {
border: `1px solid ${config?.color}`,
color: config?.color,
label: t(config?.label),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -1,7 +1,293 @@
<template>
<div> step </div>
<div class="flex flex-col gap-[16px]">
<div class="action-line">
<div class="action-group">
<a-checkbox
v-model:model-value="stepInfo.checkedAll"
:indeterminate="stepInfo.indeterminate"
@change="handleChangeAll"
/>
<div class="flex items-center gap-[4px]">
{{ t('apiScenario.sum') }}
<div class="text-[rgb(var(--primary-5))]">{{ stepInfo.steps.length }}</div>
{{ t('apiScenario.steps') }}
</div>
</div>
<div v-if="stepInfo.checkedAll || stepInfo.indeterminate" class="action-group">
<a-tooltip :content="stepInfo.isExpand ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
<a-button
type="outline"
class="expand-step-btn arco-btn-outline--secondary"
size="mini"
@click="expandAllStep"
>
<MsIcon v-if="stepInfo.isExpand" type="icon-icon_comment_collapse_text_input" />
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
</a-button>
</a-tooltip>
<a-button type="outline" size="mini" @click="batchEnable">
{{ t('common.batchEnable') }}
</a-button>
<a-button type="outline" size="mini" @click="batchDisable">
{{ t('common.batchDisable') }}
</a-button>
<a-button type="outline" size="mini" @click="batchDebug">
{{ t('common.batchDebug') }}
</a-button>
<a-button type="outline" size="mini" @click="batchDelete">
{{ t('common.batchDelete') }}
</a-button>
</div>
<template v-else>
<div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div>
<div class="text-[var(--color-text-4)]">{{ stepInfo.executeTime }}</div>
</div>
<div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div>
<div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.success') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeSuccessCount }}</div>
</div>
<div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeFailCount }}</div>
</div>
<MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton>
</div>
<div class="action-group ml-auto">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.searchByName')"
allow-clear
class="w-[200px]"
@search="searchStep"
@press-enter="searchStep"
/>
<a-button type="outline" class="arco-btn-outline--secondary !mr-0 !p-[8px]" @click="refreshStepInfo">
<template #icon>
<icon-refresh class="text-[var(--color-text-4)]" />
</template>
</a-button>
</div>
</template>
</div>
<a-dropdown
class="scenario-action-dropdown"
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
>
<a-button type="dashed" class="add-step-btn" long>
<div class="flex items-center gap-[8px]">
<icon-plus />
{{ t('apiScenario.addStep') }}
</div>
</a-button>
<template #content>
<a-dgroup :title="t('apiScenario.requestScenario')">
<a-doption :value="ScenarioAddStepActionType.IMPORT_SYSTEM_API">
{{ t('apiScenario.importSystemApi') }}
</a-doption>
<a-doption :value="ScenarioAddStepActionType.CUSTOM_API">
{{ t('apiScenario.customApi') }}
</a-doption>
</a-dgroup>
<a-dgroup :title="t('apiScenario.logicControl')">
<a-doption :value="ScenarioAddStepActionType.LOOP_CONTROL">
<div class="flex w-full items-center justify-between">
{{ t('apiScenario.loopControl') }}
<MsButton type="text" @click="openTutorial">{{ t('apiScenario.tutorial') }}</MsButton>
</div>
</a-doption>
<a-doption :value="ScenarioAddStepActionType.CONDITION_CONTROL">
{{ t('apiScenario.conditionControl') }}
</a-doption>
<a-doption :value="ScenarioAddStepActionType.ONLY_ONCE_CONTROL">
{{ t('apiScenario.onlyOnceControl') }}
</a-doption>
</a-dgroup>
<a-dgroup :title="t('apiScenario.other')">
<a-doption :value="ScenarioAddStepActionType.SCRIPT_OPERATION">
{{ t('apiScenario.scriptOperation') }}
</a-doption>
<a-doption :value="ScenarioAddStepActionType.WAIT_TIME">{{ t('apiScenario.waitTime') }}</a-doption>
</a-dgroup>
</template>
</a-dropdown>
<importApiDrawer v-model:visible="importApiDrawerVisible" />
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import dayjs from 'dayjs';
<style lang="less" scoped></style>
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import executeStatus from '../common/executeStatus.vue';
import importApiDrawer from '../common/importApiDrawer.vue';
import stepType from '../common/stepType.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
export interface ScenarioStepItem {
id: string | number;
type: ScenarioStepType;
name: string;
description: string;
status: ScenarioExecuteStatus;
children?: ScenarioStepItem[];
}
export interface ScenarioStepInfo {
id: string | number;
steps: ScenarioStepItem[];
checkedAll: boolean; //
indeterminate: boolean; //
isExpand: boolean; //
executeTime: string; //
executeSuccessCount?: number; //
executeFailCount?: number; //
}
const { t } = useI18n();
const keyword = ref('');
const stepInfo = ref<ScenarioStepInfo>({
id: new Date().getTime(),
steps: [],
checkedAll: false,
indeterminate: false,
isExpand: false,
executeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
executeSuccessCount: 0,
executeFailCount: 0,
});
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
stepInfo.value.indeterminate = false;
if (value) {
stepInfo.value.checkedAll = true;
} else {
stepInfo.value.checkedAll = false;
}
}
function expandAllStep() {
stepInfo.value.isExpand = !stepInfo.value.isExpand;
}
function batchEnable() {
console.log('批量启用');
}
function batchDisable() {
console.log('批量禁用');
}
function batchDebug() {
console.log('批量调试');
}
function batchDelete() {
console.log('批量删除');
}
function checkReport() {
console.log('查看报告');
}
function refreshStepInfo() {
console.log('刷新步骤信息');
}
function searchStep(val: string) {
stepInfo.value.steps = stepInfo.value.steps.filter((item) => item.name.includes(val));
}
const importApiDrawerVisible = ref(false);
function handleActionSelect(val: ScenarioAddStepActionType) {
switch (val) {
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
importApiDrawerVisible.value = true;
break;
case ScenarioAddStepActionType.CUSTOM_API:
console.log('自定义API');
break;
case ScenarioAddStepActionType.LOOP_CONTROL:
console.log('循环控制');
break;
case ScenarioAddStepActionType.CONDITION_CONTROL:
console.log('条件控制');
break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
console.log('仅执行一次');
break;
case ScenarioAddStepActionType.SCRIPT_OPERATION:
console.log('脚本操作');
break;
case ScenarioAddStepActionType.WAIT_TIME:
console.log('等待时间');
break;
default:
break;
}
}
function openTutorial() {
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
}
</script>
<style lang="less">
.scenario-action-dropdown {
.arco-dropdown-list-wrapper {
width: 200px;
max-height: 100%;
.arco-dropdown-option-content {
@apply flex w-full;
}
}
}
</style>
<style lang="less" scoped>
.action-line {
@apply flex items-center;
gap: 16px;
height: 32px;
.action-group {
@apply flex items-center;
gap: 8px;
.expand-step-btn {
padding: 4px;
.arco-icon {
color: var(--color-text-4);
}
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
}
}
.add-step-btn {
@apply bg-white;
padding: 4px;
border: 1px dashed rgb(var(--primary-3));
color: rgb(var(--primary-5));
&:hover,
&:focus {
border: 1px dashed rgb(var(--primary-5));
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
}
}
</style>

View File

@ -1,7 +1,157 @@
<template>
<div> create </div>
<MsSplitBox :size="0.7" :max="0.9" :min="0.7" direction="horizontal" expand-direction="right">
<template #first>
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]">
<step v-if="activeKey === ScenarioCreateComposition.STEP" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PARAMS" :title="t('apiScenario.params')" class="p-[16px]">
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="scenario.params" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PRE_POST" :title="t('apiScenario.prePost')" class="p-[16px]">
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.ASSERTION" :title="t('apiScenario.assertion')" class="p-[16px]">
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="p-[16px]">
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" />
</a-tab-pane>
</a-tabs>
</template>
<template #second>
<div class="p-[16px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="activeApiTabFormRef" :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.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:第一版先不做依赖 -->
<!-- <div class="mb-[8px] flex items-center">
<div class="text-[var(--color-text-2)]">
{{ t('apiTestManagement.addDependency') }}
</div>
<a-divider margin="4px" direction="vertical" />
<MsButton
type="text"
class="font-medium"
:disabled="scenario.preDependency.length === 0 && scenario.postDependency.length === 0"
@click="clearAllDependency"
>
{{ t('apiTestManagement.clearSelected') }}
</MsButton>
</div>
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="flex items-center">
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
{{ t('apiTestManagement.preDependency') }}
<div class="text-[rgb(var(--primary-5))]">
{{ scenario.preDependency.length }}
</div>
{{ t('apiTestManagement.dependencyUnit') }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="handleDddDependency('pre')">
{{ t('apiTestManagement.addPreDependency') }}
</MsButton>
</div>
<div class="mt-[8px] flex items-center">
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
{{ t('apiTestManagement.postDependency') }}
<div class="text-[rgb(var(--primary-5))]">
{{ scenario.postDependency.length }}
</div>
{{ t('apiTestManagement.dependencyUnit') }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="handleDddDependency('post')">
{{ t('apiTestManagement.addPostDependency') }}
</MsButton>
</div>
</div> -->
</div>
</template>
</MsSplitBox>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
<style lang="less" scoped></style>
import { useI18n } from '@/hooks/useI18n';
import { ModuleTreeNode } from '@/models/common';
import { ApiScenarioStatus, RequestCaseStatus, ScenarioCreateComposition } 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 setting = defineAsyncComponent(() => import('../components/setting.vue'));
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
}>();
const { t } = useI18n();
const activeKey = ref<ScenarioCreateComposition>(ScenarioCreateComposition.STEP);
const scenario = ref<any>({
name: '',
moduleId: 'root',
status: RequestCaseStatus.PROCESSING,
tags: [],
params: [],
});
</script>
<style lang="less" scoped>
:deep(.arco-tabs-content) {
@apply pt-0;
}
</style>

View File

@ -109,7 +109,6 @@
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';
@ -126,14 +125,14 @@
const allParams = ref<any[]>([]);
const props = defineProps<{
detail: RequestParam;
detail: Record<string, any>;
}>();
const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard();
const { t } = useI18n();
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
const previewDetail = ref<Record<string, any>>(cloneDeep(props.detail));
const description = computed(() => [
{

View File

@ -52,7 +52,12 @@
</template>
</MsSplitBox>
</div>
<detail v-else :detail="activeApiTab"></detail>
<div v-else-if="activeApiTab.is" class="pageWrap">
<detail :detail="activeApiTab"></detail>
</div>
<div v-else class="pageWrap">
<create :module-tree="folderTree"></create>
</div>
</MsCard>
</template>
@ -65,10 +70,10 @@
import MsCard from '@/components/pure/ms-card/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import scenarioModuleTree from './components/scenarioModuleTree.vue';
import detail from './detail/index.vue';
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
import { useI18n } from '@/hooks/useI18n';
@ -76,22 +81,27 @@
import { ApiScenarioGetModuleParams } from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
//
const detail = defineAsyncComponent(() => import('./detail/index.vue'));
const create = defineAsyncComponent(() => import('./create/index.vue'));
const { t } = useI18n();
const apiTabs = ref<any[]>([
const apiTabs = ref<TabItem[]>([
{
id: 'all',
label: t('apiScenario.allScenario'),
closable: false,
},
]);
const activeApiTab = ref<any>(apiTabs.value[0]);
const activeApiTab = ref<TabItem>(apiTabs.value[0]);
function newTab() {
apiTabs.value.push({
id: `newTab${apiTabs.value.length}`,
label: `New Tab ${apiTabs.value.length}`,
closable: true,
isNew: true,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
}

View File

@ -33,7 +33,6 @@ export default {
'apiScenario.params.tag': 'Tag',
'apiScenario.params.desc': 'Description',
'apiScenario.table.showChildrenModuleScenario': 'Show subdirectory scenarios',
'apiScenario.table.columns.name': 'Name',
'apiScenario.table.columns.level': 'Level',
'apiScenario.table.columns.status': 'status',
@ -47,9 +46,7 @@ export default {
'apiScenario.table.columns.createTime': 'Create time',
'apiScenario.table.columns.updateUser': 'Update user',
'apiScenario.table.columns.updateTime': 'Update time',
'apiScenario.execute': 'Execute',
// 批量操作文案
'api_scenario.batch_operation.success': 'Success {opt} to {name}',
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios',

View File

@ -32,7 +32,6 @@ export default {
'apiScenario.params.tag': '标签',
'apiScenario.params.desc': '描述',
'apiScenario.table.showChildrenModuleScenario': '显示子目录场景',
'apiScenario.table.columns.name': '场景名称',
'apiScenario.table.columns.level': '场景等级',
'apiScenario.table.columns.status': '状态',
@ -46,7 +45,6 @@ export default {
'apiScenario.table.columns.createTime': '创建时间',
'apiScenario.table.columns.updateUser': '更新人',
'apiScenario.table.columns.updateTime': '更新时间',
'api_scenario.table.searchPlaceholder': '通过 ID/名称/标签搜索',
'api_scenario.table.batchModalSubTitle': '(已选 {count} 个场景)',
'api_scenario.table.chooseAttr': '选择属性',
@ -60,10 +58,44 @@ export default {
'api_scenario.table.status.underway': '进行中',
'api_scenario.table.status.completed': '已完成',
'api_scenario.table.status.deprecate': '已废弃',
'apiScenario.execute': '执行',
// 批量操作文案
'api_scenario.batch_operation.success': '成功{opt}至 {name}',
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',
'apiScenario.name': '场景名称',
'apiScenario.nameRequired': '场景名称不能为空',
'apiScenario.namePlaceholder': '请输入场景名称',
'apiScenario.belongModule': '所属模块',
'apiScenario.level': '场景等级',
'apiScenario.status': '场景状态',
'apiScenario.addStep': '添加步骤',
'apiScenario.requestScenario': '请求/场景',
'apiScenario.importSystemApi': '导入系统请求',
'apiScenario.customApi': '自定义请求',
'apiScenario.logicControl': '逻辑控制',
'apiScenario.loopControl': '循环控制器',
'apiScenario.tutorial': '使用教程',
'apiScenario.conditionControl': '条件控制器',
'apiScenario.onlyOnceControl': '仅一次控制器',
'apiScenario.other': '其他',
'apiScenario.scriptOperation': '脚本操作',
'apiScenario.waitTime': '等待时间',
'apiScenario.quoteApi': '引用 API',
'apiScenario.copyApi': '复制 API',
'apiScenario.quoteCase': '引用 CASE',
'apiScenario.copyCase': '复制 CASE',
'apiScenario.quoteScenario': '引用场景',
'apiScenario.copyScenario': '复制场景',
'apiScenario.sum': '共',
'apiScenario.steps': '个步骤',
'apiScenario.expandAllStep': '展开全部子步骤',
'apiScenario.collapseAllStep': '折叠全部子步骤',
'apiScenario.executeTime': '执行时间:',
'apiScenario.executeResult': '执行结果',
'apiScenario.checkReport': '查看报告',
'apiScenario.searchByName': '通过名称搜索',
'apiScenario.api': '接口',
'apiScenario.case': '用例',
'apiScenario.scenario': '场景',
'apiScenario.sumSelected': '共选择',
};