feat(接口场景): 场景步骤 2%&部分 bug 解决
This commit is contained in:
parent
868a1a38cb
commit
a158207ce5
|
@ -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]' : '',
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -136,4 +136,9 @@ export default {
|
|||
'common.share': '分享',
|
||||
'common.notRemind': '不再提醒',
|
||||
'common.status': '状态',
|
||||
'common.batchEnable': '批量启用',
|
||||
'common.batchDisable': '批量禁用',
|
||||
'common.batchDelete': '批量删除',
|
||||
'common.batchDebug': '批量调试',
|
||||
'common.quote': '引用',
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
// 如果查看的tab被删除了,则切换到第一个tab
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(() => [
|
||||
{
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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': '共选择',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue