feat(接口场景): 场景步骤 5%

This commit is contained in:
baiqi 2024-03-16 17:46:27 +08:00 committed by 刘瑞斌
parent e76308113e
commit f624884fd7
13 changed files with 554 additions and 229 deletions

View File

@ -37,7 +37,7 @@ import {
ScenarioHistoryItem,
ScenarioHistoryPageParams,
} from '@/models/apiTest/scenario';
import { AddModuleParams, BatchApiParams, CommonList, ModuleTreeNode, MoveModules } from '@/models/common';
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules } from '@/models/common';
// 更新模块
export function updateModule(data: ApiScenarioModuleUpdateParams) {

View File

@ -5,7 +5,8 @@
v-bind="props"
ref="treeRef"
v-model:expanded-keys="expandedKeys"
v-model:selected-keys="innerSelectedKeys"
v-model:selected-keys="selectedKeys"
v-model:checked-keys="checkedKeys"
:data="treeData"
class="ms-tree"
:allow-drop="handleAllowDrop"
@ -20,6 +21,7 @@
:content="_props[props.fieldNames.title]"
:mouse-enter-delay="800"
:position="props.titleTooltipPosition"
:disabled="props.disabledTitleTooltip"
>
<slot name="title" v-bind="_props"></slot>
</a-tooltip>
@ -32,34 +34,31 @@
v-if="_props.hideMoreAction !== true"
:class="[
'ms-tree-node-extra',
innerFocusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '', // TODO:
focusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '', // TODO:
]"
>
<div
class="ml-[-4px] flex h-[32px] items-center rounded-[var(--border-radius-small)] bg-[rgb(var(--primary-1))]"
<slot name="extra" v-bind="_props"></slot>
<MsTableMoreAction
v-if="props.nodeMoreActions"
:list="
typeof props.filterMoreActionFunc === 'function'
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
: props.nodeMoreActions
"
trigger="click"
@select="handleNodeMoreSelect($event, _props)"
@close="moreActionsClose"
>
<slot name="extra" v-bind="_props"></slot>
<MsTableMoreAction
v-if="props.nodeMoreActions"
:list="
typeof props.filterMoreActionFunc === 'function'
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
: props.nodeMoreActions
"
trigger="click"
@select="handleNodeMoreSelect($event, _props)"
@close="moreActionsClose"
<MsButton
type="text"
:size="props.nodeMoreActionSize || 'mini'"
class="ms-tree-node-extra__more"
@click="focusNodeKey = _props[props.fieldNames.key]"
>
<MsButton
type="text"
size="mini"
class="ms-tree-node-extra__more"
@click="innerFocusNodeKey = _props[props.fieldNames.key]"
>
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsTableMoreAction>
</div>
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsTableMoreAction>
<slot name="extraEnd" v-bind="_props"></slot>
</div>
</template>
</a-tree>
@ -75,8 +74,7 @@
</template>
<script setup lang="ts">
import { h, nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
import { useVModel } from '@vueuse/core';
import { nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
import { debounce } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -101,15 +99,14 @@
defaultExpandAll?: boolean; //
selectable?: boolean | ((node: MsTreeNodeData, info: { level: number; isLeaf: boolean }) => boolean); //
fieldNames?: MsTreeFieldNames; //
focusNodeKey?: string | number; // key
selectedKeys?: Array<string | number>; // key
nodeMoreActions?: ActionsItem[]; //
nodeMoreActionSize?: 'medium' | 'mini' | 'small' | 'large'; //
expandAll?: boolean; // /true false
emptyText?: string; //
checkable?: boolean; //
checkedStrategy?: 'all' | 'parent' | 'child'; //
checkedKeys?: Array<string | number>; // key
virtualListProps?: VirtualListProps; //
disabledTitleTooltip?: boolean; // tooltip
titleTooltipPosition?:
| 'top'
| 'tl'
@ -138,6 +135,7 @@
children: 'children',
isLeaf: 'isLeaf',
}),
disabledTitleTooltip: false,
}
);
@ -151,12 +149,23 @@
dropPosition: number // -1 1 0
): void;
(e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void;
(e: 'update:focusNodeKey', val: string | number): void;
(e: 'update:selectedKeys', val: Array<string | number>): void;
(e: 'moreActionsClose'): void;
(e: 'check', val: Array<string | number>): void;
}>();
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
default: [],
});
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
default: [],
});
const expandedKeys = defineModel<(string | number)[]>('expandedKeys', {
default: [],
});
const focusNodeKey = defineModel<string | number>('focusNodeKey', {
default: '',
});
const treeContainerRef: Ref = ref(null);
const treeRef: Ref = ref(null);
const { isInitListener, containerStatusClass, setContainer, initScrollListener } = useContainerShadow({
@ -166,21 +175,22 @@
const originalTreeData = ref<MsTreeNodeData[]>([]);
function init(isFirstInit = false) {
originalTreeData.value = mapTree<MsTreeNodeData>(props.data, (node: MsTreeNodeData) => {
if (!props.showLine) {
// 线线 switcherIcon switcherIcon
node.icon = () => h('span', { class: 'hidden' });
}
if (
node[props.fieldNames.isLeaf || 'isLeaf'] ||
!node[props.fieldNames.children] ||
node[props.fieldNames.children]?.length === 0
) {
// icon线 switcherIcon 线 icon
node[props.showLine ? 'switcherIcon' : 'icon'] = () => h('span', { class: 'hidden' });
}
return node;
});
originalTreeData.value = mapTree<MsTreeNodeData>(props.data);
// (node: MsTreeNodeData) => {
// // if (!props.showLine) {
// // // 线线 switcherIcon switcherIcon
// // node.icon = () => h('span', { class: 'hidden' });
// // }
// // if (
// // node[props.fieldNames.isLeaf || 'isLeaf'] ||
// // !node[props.fieldNames.children] ||
// // node[props.fieldNames.children]?.length === 0
// // ) {
// // // icon线 switcherIcon 线 icon
// // node[props.showLine ? 'switcherIcon' : 'icon'] = () => h('span', { class: 'hidden' });
// // }
// return node;
// });
nextTick(() => {
if (isFirstInit) {
if (props.defaultExpandAll) {
@ -209,8 +219,6 @@
}
);
const expandedKeys = ref<(string | number)[]>([]);
/**
* 根据关键字过滤树节点
* @param keyword 搜索关键字
@ -327,20 +335,18 @@
/**
* 处理树节点选中非复选框
*/
function select(selectedKeys: Array<string | number>, data: MsTreeSelectedData) {
emit('select', selectedKeys, data.selectedNodes[0]);
function select(_selectedKeys: Array<string | number>, data: MsTreeSelectedData) {
emit('select', _selectedKeys, data.selectedNodes[0]);
}
function checked(checkedKeys: Array<string | number>) {
emit('check', checkedKeys);
function checked(_checkedKeys: Array<string | number>) {
emit('check', _checkedKeys);
}
const innerFocusNodeKey = useVModel(props, 'focusNodeKey', emit); //
const focusEl = ref<HTMLElement | null>(); //
watch(
() => innerFocusNodeKey.value,
() => focusNodeKey.value,
(val) => {
if (val?.toString() !== '') {
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
@ -374,7 +380,18 @@
}
);
const innerSelectedKeys = useVModel(props, 'selectedKeys', emit);
function checkAll(val: boolean) {
treeRef.value?.checkAll(val);
}
function expandNode(key: string | number, expanded: boolean) {
treeRef.value?.expandNode(key, expanded);
}
defineExpose({
checkAll,
expandNode,
});
</script>
<style lang="less">
@ -443,7 +460,12 @@
width: 60%;
}
.ms-tree-node-extra {
@apply invisible relative w-0;
@apply invisible relative flex w-0 items-center;
margin-left: -4px;
height: 32px;
border-radius: var(--border-radius-small);
background-color: rgb(var(--primary-1));
&:hover {
@apply visible w-auto;
}

View File

@ -138,4 +138,5 @@ export default {
'common.batchDelete': 'Batch delete',
'common.batchDebug': 'Batch debug',
'common.quote': 'Quote',
'common.execute': '执行',
};

View File

@ -141,4 +141,5 @@ export default {
'common.batchDelete': '批量删除',
'common.batchDebug': '批量调试',
'common.quote': '引用',
'common.execute': '执行',
};

View File

@ -199,7 +199,8 @@ export function mapTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>, path: string) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children',
parentPath = ''
parentPath = '',
level = 0
): T[] {
if (!Array.isArray(tree)) {
tree = [tree];
@ -210,8 +211,17 @@ export function mapTree<T>(
const fullPath = node.path ? `${parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
if (newNode && newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
newNode[customChildrenKey] = mapTree(newNode[customChildrenKey], customNodeFn, customChildrenKey);
if (newNode) {
newNode.level = level;
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
newNode[customChildrenKey] = mapTree(
newNode[customChildrenKey],
customNodeFn,
customChildrenKey,
fullPath,
level + 1
);
}
}
return newNode;

View File

@ -25,7 +25,13 @@
@change="resetModuleAndTable"
/>
</div>
<moduleTree ref="moduleTreeRef" :type="activeKey" :protocol="protocol" @select="handleModuleSelect" />
<moduleTree
ref="moduleTreeRef"
:type="activeKey"
:project-id="currentProject"
:protocol="protocol"
@select="handleModuleSelect"
/>
</div>
</div>
<div class="table-container">
@ -143,7 +149,7 @@
const moduleIds = ref<(string | number)[]>([]);
function resetModuleAndTable() {
moduleTreeRef.value?.init();
moduleTreeRef.value?.init(activeKey.value);
apiTableRef.value?.loadPage(['root']); // id
}
@ -180,6 +186,9 @@
if (val) {
resetModuleAndTable();
}
},
{
immediate: true,
}
);

View File

@ -54,8 +54,11 @@
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getModuleCount, getModuleTreeOnlyModules } from '@/api/modules/api-test/management';
import {
getModuleCount as getScenarioModuleCount,
getModuleTree as getScenarioModuleTree,
} from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common';
@ -64,6 +67,7 @@
defineProps<{
type: 'api' | 'case' | 'scenario';
protocol: string;
projectId: string;
}>(),
{
type: 'api',
@ -73,7 +77,6 @@
(e: 'select', ids: (string | number)[], node: MsTreeNodeData): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const moduleKeyword = ref('');
@ -86,16 +89,21 @@
/**
* 初始化模块树
*/
async function initModules() {
async function initModules(type = props.type) {
try {
loading.value = true;
folderTree.value = await getModuleTreeOnlyModules({
//
const params = {
keyword: moduleKeyword.value,
protocol: props.protocol,
projectId: appStore.currentProjectId,
projectId: props.projectId,
moduleIds: [],
});
};
if (type === 'api' || type === 'case') {
// case api
folderTree.value = await getModuleTreeOnlyModules(params);
} else if (type === 'scenario') {
folderTree.value = await getScenarioModuleTree(params);
}
selectedKeys.value = [folderTree.value[0]?.id];
} catch (error) {
// eslint-disable-next-line no-console
@ -105,14 +113,20 @@
}
}
async function initModuleCount() {
async function initModuleCount(type = props.type) {
try {
moduleCountMap.value = await getModuleCount({
const params = {
keyword: moduleKeyword.value,
protocol: props.protocol,
projectId: appStore.currentProjectId,
projectId: props.projectId,
moduleIds: [],
});
};
if (type === 'api' || type === 'case') {
// case api
moduleCountMap.value = await getModuleCount(params);
} else if (type === 'scenario') {
moduleCountMap.value = await getScenarioModuleCount(params);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -128,9 +142,9 @@
emit('select', [keys[0], ...offspringIds], node);
}
function init() {
initModules();
initModuleCount();
function init(type = props.type) {
initModules(type);
initModuleCount(type);
}
defineExpose({

View File

@ -129,7 +129,7 @@
sorter: true,
},
fixed: 'left',
width: 100,
width: 120,
},
{
title: 'apiTestManagement.apiName',

View File

@ -0,0 +1,55 @@
<template>
<a-dropdown class="scenario-action-dropdown" @select="(val) => emit('select', val as ScenarioAddStepActionType)">
<slot></slot>
<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>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioAddStepActionType } from '@/enums/apiEnum';
const emit = defineEmits<{
(e: 'select', val: ScenarioAddStepActionType): void;
}>();
const { t } = useI18n();
function openTutorial() {
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
}
</script>
<style lang="less" scoped></style>

View File

@ -3,8 +3,9 @@
<div class="action-line">
<div class="action-group">
<a-checkbox
v-model:model-value="stepInfo.checkedAll"
:indeterminate="stepInfo.indeterminate"
v-show="stepInfo.steps.length > 0"
v-model:model-value="checkedAll"
:indeterminate="indeterminate"
@change="handleChangeAll"
/>
<div class="flex items-center gap-[4px]">
@ -13,32 +14,35 @@
{{ 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')">
<div class="action-group">
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
<a-button
v-show="stepInfo.steps.length > 0"
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-if="isExpandAll" 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>
<template v-if="checkedAll || indeterminate">
<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>
</template>
</div>
<template v-else>
<template v-if="stepInfo.executeTime">
<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>
@ -55,126 +59,137 @@
</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 v-if="!checkedAll && !indeterminate" 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
v-if="!props.isNew"
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>
</div>
<div>
<stepTree ref="stepTreeRef" v-model:checked-keys="checkedKeys" :steps="stepInfo.steps" />
</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">
import dayjs from 'dayjs';
// import dayjs from 'dayjs';
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/index.vue';
import stepType from '../common/stepType.vue';
import stepTree, { ScenarioStepItem } from './stepTree.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[];
}
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
export interface ScenarioStepInfo {
id: string | number;
steps: ScenarioStepItem[];
checkedAll: boolean; //
indeterminate: boolean; //
isExpand: boolean; //
executeTime: string; //
executeTime?: string; //
executeSuccessCount?: number; //
executeFailCount?: number; //
}
const props = defineProps<{
isNew?: boolean; //
}>();
const { t } = useI18n();
const checkedAll = ref(false); //
const indeterminate = ref(false); //
const isExpandAll = ref(false); //
const checkedKeys = ref<string[]>([]); // key
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
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'),
steps: [
{
id: 1,
order: 1,
checked: false,
type: ScenarioStepType.CUSTOM_API,
name: 'API1',
description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS,
children: [
{
id: 11,
order: 1,
checked: false,
type: ScenarioStepType.CUSTOM_API,
name: 'API11',
description: 'API11描述',
status: ScenarioExecuteStatus.SUCCESS,
},
{
id: 12,
order: 2,
checked: false,
type: ScenarioStepType.CUSTOM_API,
name: 'API12',
description: 'API12描述',
status: ScenarioExecuteStatus.SUCCESS,
},
],
},
{
id: 2,
order: 2,
checked: false,
type: ScenarioStepType.CUSTOM_API,
name: 'API1',
description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS,
},
],
executeTime: '',
executeSuccessCount: 0,
executeFailCount: 0,
});
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
stepInfo.value.indeterminate = false;
indeterminate.value = false;
if (value) {
stepInfo.value.checkedAll = true;
checkedAll.value = true;
} else {
stepInfo.value.checkedAll = false;
checkedAll.value = false;
}
stepTreeRef.value?.checkAll(checkedAll.value);
}
watch(checkedKeys, (val) => {
if (val.length === 0) {
checkedAll.value = false;
indeterminate.value = false;
} else if (val.length === stepInfo.value.steps.length) {
checkedAll.value = true;
indeterminate.value = false;
} else {
checkedAll.value = false;
indeterminate.value = true;
}
});
function expandAllStep() {
stepInfo.value.isExpand = !stepInfo.value.isExpand;
isExpandAll.value = !isExpandAll.value;
}
function batchEnable() {
@ -204,40 +219,6 @@
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">
@ -277,17 +258,4 @@
}
}
}
.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

@ -0,0 +1,241 @@
<template>
<div class="flex flex-col gap-[16px]">
<MsTree
ref="treeRef"
v-model:checked-keys="checkedKeys"
v-model:focus-node-key="focusStepKey"
:data="props.steps"
:node-more-actions="stepMoreActions"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:selectable="false"
disabled-title-tooltip
checkable
block-node
draggable
>
<template #title="step">
<div class="flex items-center gap-[8px]">
<div
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
>
{{ step.order }}
</div>
<div class="step-node-first">
<div
v-show="step.children?.length > 0"
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"
@click.stop="toggleNodeExpand(step)"
>
<MsIcon
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
:size="14"
/>
{{ step.children?.length || 0 }}
</div>
<div class="text-[var(--color-text-1)]">{{ step.name }}</div>
</div>
</div>
</template>
<template #extra="step">
<MsButton :id="step.key" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="setFocusNodeKey(step)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</template>
<template #extraEnd="step">
<executeStatus :status="step.status" size="small" />
</template>
</MsTree>
<actionDropdown
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>
</actionDropdown>
<importApiDrawer v-model:visible="importApiDrawerVisible" />
</div>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue';
import importApiDrawer from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue';
import actionDropdown from './actionDropdown.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
export interface ScenarioStepItem {
id: string | number;
order: number;
checked: boolean;
type: ScenarioStepType;
name: string;
description: string;
status: ScenarioExecuteStatus;
children?: ScenarioStepItem[];
}
const props = defineProps<{
steps: ScenarioStepItem[];
}>();
const { t } = useI18n();
const checkedKeys = defineModel<string[]>('checkedKeys', {
required: true,
});
const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string>(''); // key
const stepMoreActions: ActionsItem[] = [
{
label: 'common.execute',
eventTag: 'execute',
},
{
label: 'common.copy',
eventTag: 'copy',
},
{
label: 'apiScenario.scenarioConfig',
eventTag: 'config',
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
];
function setFocusNodeKey(node: MsTreeNodeData) {
focusStepKey.value = node.id || '';
}
function toggleNodeExpand(node: MsTreeNodeData) {
if (node.id) {
treeRef.value?.expandNode(node.id, !node.expanded);
}
}
function checkAll(val: boolean) {
treeRef.value?.checkAll(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;
}
}
defineExpose({
checkAll,
});
</script>
<style lang="less" scoped>
.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));
}
}
//
.loop-levels(@index, @max) when (@index <= @max) {
:deep(.arco-tree-node[data-level='@{index}']) {
margin-left: @index * 32px;
}
.loop-levels(@index + 1, @max); //
}
.loop-levels(0, 99); //
:deep(.arco-tree-node) {
padding: 7px 8px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-medium) !important;
&:not(:first-child) {
margin-top: 4px;
}
&:hover {
background-color: var(--color-text-n9) !important;
.arco-tree-node-title {
background-color: var(--color-text-n9) !important;
}
}
.arco-tree-node-title {
&:hover {
background-color: var(--color-text-n9) !important;
}
.step-node-first {
@apply flex items-center;
gap: 8px;
}
&[draggable='true']:hover {
.step-node-first {
padding-left: 20px;
}
}
}
.arco-tree-node-indent {
@apply hidden;
}
.arco-tree-node-switcher {
@apply hidden;
}
.arco-tree-node-drag-icon {
@apply ml-0;
top: 6px;
left: 24px;
width: 16px;
height: 16px;
.arco-icon {
font-size: 16px !important;
}
}
.ms-tree-node-extra {
gap: 4px;
background-color: var(--color-text-n9) !important;
}
}
</style>

View File

@ -3,7 +3,7 @@
<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" />
<step v-if="activeKey === ScenarioCreateComposition.STEP" is-new />
</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" />
@ -127,6 +127,8 @@
import { ModuleTreeNode } from '@/models/common';
import { ApiScenarioStatus, RequestCaseStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
import type { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
//
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
const params = defineAsyncComponent(() => import('../components/params.vue'));
@ -144,6 +146,7 @@
const scenario = ref<any>({
name: '',
moduleId: 'root',
stepInfo: {} as ScenarioStepInfo,
status: RequestCaseStatus.PROCESSING,
tags: [],
params: [],

View File

@ -98,6 +98,7 @@ export default {
'apiScenario.case': '用例',
'apiScenario.scenario': '场景',
'apiScenario.sumSelected': '共选择',
'apiScenario.scenarioConfig': '场景配置',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号',