feat(接口场景): 场景步骤 15%
This commit is contained in:
parent
393bcabc3e
commit
488a93ea2c
|
@ -167,11 +167,11 @@
|
|||
</a-form>
|
||||
<div
|
||||
v-if="paramSettingType === 'mock' && paramForm.type !== ''"
|
||||
class="mb-[16px] flex items-center gap-[16px] bg-[var(--color-text-n9)] p-[5px_8px]"
|
||||
class="mb-[16px] flex items-baseline gap-[16px] overflow-hidden bg-[var(--color-text-n9)] p-[5px_8px]"
|
||||
>
|
||||
<div class="text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div>
|
||||
<a-spin :loading="previewLoading" class="flex gap-[8px]">
|
||||
<div class="text-[var(--color-text-1)]">{{ paramPreview }}</div>
|
||||
<div class="break-all text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div>
|
||||
<a-spin :loading="previewLoading" class="flex flex-1 flex-wrap gap-[8px]">
|
||||
<div class="param-preview">{{ paramPreview }}</div>
|
||||
<MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
@ -625,6 +625,13 @@
|
|||
overflow-y: auto;
|
||||
margin-right: -6px;
|
||||
max-height: 400px;
|
||||
.ms-params-input-setting-trigger-content-scroll-preview {
|
||||
@apply w-full overflow-y-auto overflow-x-hidden break-all;
|
||||
.ms-scroll-bar();
|
||||
|
||||
max-height: 100px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,13 +24,15 @@
|
|||
:position="props.titleTooltipPosition"
|
||||
:disabled="props.disabledTitleTooltip"
|
||||
>
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
<span :class="props.titleClass || 'ms-tree-node-title'">
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-if="$slots['drag-icon']" #drag-icon="_props">
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
</template>
|
||||
<template v-if="$slots['extra']" #extra="_props">
|
||||
<template v-if="$slots['extra'] || props.nodeMoreActions" #extra="_props">
|
||||
<div
|
||||
v-if="_props.hideMoreAction !== true"
|
||||
:class="[
|
||||
|
@ -91,6 +93,7 @@
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
keyword?: string; // 搜索关键字
|
||||
titleClass?: string; // 标题样式类
|
||||
searchDebounce?: number; // 搜索防抖 ms 数
|
||||
draggable?: boolean; // 是否可拖拽
|
||||
blockNode?: boolean; // 是否块级节点
|
||||
|
@ -509,8 +512,11 @@
|
|||
.arco-tree-node-title {
|
||||
font-weight: 500 !important;
|
||||
color: rgb(var(--primary-5));
|
||||
* {
|
||||
.ms-tree-node-title {
|
||||
color: rgb(var(--primary-5));
|
||||
* {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
type?: TagType; // tag类型
|
||||
size?: Size; // tag尺寸
|
||||
theme?: Theme; // tag主题
|
||||
selfStyle?: any; // 自定义样式
|
||||
selfStyle?: Record<string, any>; // 自定义样式
|
||||
width?: number; // tag宽度,不传入时绑定max-width
|
||||
maxWidth?: string;
|
||||
noMargin?: boolean; // tag之间是否有间距
|
||||
|
|
|
@ -345,6 +345,68 @@ export function findNodePathByKey<T>(
|
|||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 在某个节点前插入新节点
|
||||
* @param treeArr 目标树
|
||||
* @param targetKey 目标节点唯一值
|
||||
* @param newNode 新节点
|
||||
* @param position 插入位置
|
||||
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
||||
*/
|
||||
export function insertNode<T>(
|
||||
treeArr: TreeNode<T>[],
|
||||
targetKey: string,
|
||||
newNode: TreeNode<T>,
|
||||
position: 'before' | 'after',
|
||||
customKey = 'key'
|
||||
): void {
|
||||
function insertNodeInTree(tree: TreeNode<T>[], parent?: TreeNode<T>): boolean {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (node[customKey] === targetKey) {
|
||||
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后插入新节点
|
||||
const childrenArray = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
|
||||
const index = childrenArray.findIndex((item) => item[customKey] === node[customKey]);
|
||||
if (position === 'before') {
|
||||
childrenArray.splice(index, 0, newNode);
|
||||
} else if (position === 'after') {
|
||||
childrenArray.splice(index + 1, 0, newNode);
|
||||
}
|
||||
// 插入后返回 true
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(node.children) && insertNodeInTree(node.children, node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
insertNodeInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除树形数组中的某个节点
|
||||
* @param treeArr 目标树
|
||||
* @param targetKey 目标节点唯一值
|
||||
*/
|
||||
export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string, customKey = 'key'): void {
|
||||
function deleteNodeInTree(tree: TreeNode<T>[]): void {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (node[customKey] === targetKey) {
|
||||
tree.splice(i, 1); // 直接删除当前节点
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
deleteNodeInTree(node.children); // 递归删除子节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteNodeInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 找出俩数组之间的差异项并返回
|
||||
* @param targetMap 目标项
|
||||
|
|
|
@ -1245,6 +1245,8 @@
|
|||
|
||||
/**
|
||||
* 保存请求
|
||||
* @param fullParams 保存时传入的参数
|
||||
* @param silence 是否静默保存(接口定义另存为用例时要先静默保存接口)
|
||||
*/
|
||||
async function realSave(fullParams?: Record<string, any>, silence?: boolean) {
|
||||
try {
|
||||
|
@ -1278,6 +1280,7 @@
|
|||
requestVModel.value.label = res.name;
|
||||
requestVModel.value.url = res.path;
|
||||
requestVModel.value.path = res.path;
|
||||
requestVModel.value.moduleId = res.moduleId;
|
||||
if (!props.isDefinition) {
|
||||
saveModalVisible.value = false;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
<a-spin class="h-[calc(100%-98px)] w-full" :loading="loading">
|
||||
<a-spin class="max-h-[calc(100%-98px)] w-full" :loading="loading">
|
||||
<MsTree
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:focus-node-key="focusNodeKey"
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<a-dropdown class="scenario-action-dropdown" @select="(val) => emit('select', val as ScenarioAddStepActionType)">
|
||||
<a-dropdown
|
||||
v-model:popup-visible="visible"
|
||||
:position="props.position || 'bottom'"
|
||||
:popup-translate="props.popupTranslate"
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => emit('select', val as ScenarioAddStepActionType)"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #content>
|
||||
<a-dgroup :title="t('apiScenario.requestScenario')">
|
||||
|
@ -35,18 +41,29 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TriggerPopupTranslate } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioAddStepActionType } from '@/enums/apiEnum';
|
||||
|
||||
import { DropdownPosition } from '@arco-design/web-vue/es/dropdown/interface';
|
||||
|
||||
const props = defineProps<{
|
||||
position?: DropdownPosition;
|
||||
popupTranslate?: TriggerPopupTranslate;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', val: ScenarioAddStepActionType): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
function openTutorial() {
|
||||
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<div class="flex h-full flex-col gap-[16px]">
|
||||
<div class="action-line">
|
||||
<div class="action-group">
|
||||
<a-checkbox
|
||||
|
@ -79,7 +79,7 @@
|
|||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-[calc(100%-48px)]">
|
||||
<stepTree
|
||||
ref="stepTreeRef"
|
||||
v-model:steps="stepInfo.steps"
|
||||
|
@ -99,6 +99,7 @@
|
|||
import stepTree, { ScenarioStepItem } from './stepTree.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
|
@ -114,6 +115,7 @@
|
|||
isNew?: boolean; // 是否新建
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const checkedAll = ref(false); // 是否全选
|
||||
|
@ -127,6 +129,7 @@
|
|||
steps: [
|
||||
{
|
||||
id: 1,
|
||||
num: 10086,
|
||||
order: 1,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
|
@ -135,6 +138,9 @@
|
|||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
method: RequestMethods.GET,
|
||||
belongProjectId: appStore.currentProjectId,
|
||||
belongProjectName: '项目名称',
|
||||
actionDropdownVisible: false,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
|
@ -146,6 +152,10 @@
|
|||
name: 'API11',
|
||||
description: 'API11描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
num: 100861,
|
||||
belongProjectId: '989d23d23d',
|
||||
belongProjectName: '项目名称1',
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
|
@ -157,6 +167,10 @@
|
|||
name: 'API12',
|
||||
description: 'API12描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
num: 100862,
|
||||
belongProjectId: '989d23d23d',
|
||||
belongProjectName: '项目名称2',
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -170,6 +184,7 @@
|
|||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
|
@ -181,6 +196,7 @@
|
|||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
],
|
||||
executeTime: '',
|
||||
|
|
|
@ -1,7 +1,86 @@
|
|||
<template>
|
||||
<div>quote </div>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<a-popover position="bl" content-class="detail-popover" arrow-class="hidden">
|
||||
<MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<div>
|
||||
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
|
||||
<div class="text-[14px] text-[var(--color-text-1)]">
|
||||
{{ props.data.belongProjectName }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div>
|
||||
<div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail">
|
||||
{{ `【${props.data.num}】${props.data.name}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<MsTag
|
||||
v-if="props.data.belongProjectId !== props.data.currentProjectId"
|
||||
theme="outline"
|
||||
size="small"
|
||||
:self-style="{
|
||||
color: 'var(--color-text-4)',
|
||||
border: '1px solid var(--color-text-input-border)',
|
||||
backgroundColor: 'transparent',
|
||||
}"
|
||||
>
|
||||
{{ t('apiScenario.crossProject') }}
|
||||
</MsTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
|
||||
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
id: string | number;
|
||||
belongProjectId: string;
|
||||
belongProjectName: string;
|
||||
num: number;
|
||||
name: string;
|
||||
type: ScenarioStepType;
|
||||
currentProjectId: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
function goDetail() {
|
||||
switch (props.data.type) {
|
||||
case ScenarioStepType.COPY_API:
|
||||
case ScenarioStepType.QUOTE_API:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id });
|
||||
break;
|
||||
case ScenarioStepType.QUOTE_SCENARIO:
|
||||
case ScenarioStepType.COPY_SCENARIO:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id });
|
||||
break;
|
||||
case ScenarioStepType.COPY_CASE:
|
||||
case ScenarioStepType.QUOTE_CASE:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail-popover {
|
||||
width: 350px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<div class="max-h-[calc(100vh-305px)]">
|
||||
<div class="flex h-full flex-col gap-[16px]">
|
||||
<a-spin class="max-h-[calc(100%-46px)] w-full" :loading="loading">
|
||||
<MsTree
|
||||
ref="treeRef"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:focus-node-key="focusStepKey"
|
||||
v-model:data="steps"
|
||||
|
@ -10,20 +11,23 @@
|
|||
:expand-all="props.expandAll"
|
||||
:node-more-actions="stepMoreActions"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:selectable="false"
|
||||
:virtual-list-props="{
|
||||
height: '100%',
|
||||
threshold: 200,
|
||||
threshold: 20,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
}"
|
||||
title-class="step-tree-node-title"
|
||||
node-highlight-background-color="var(--color-text-n9)"
|
||||
action-on-node-click="expand"
|
||||
disabled-title-tooltip
|
||||
checkable
|
||||
block-node
|
||||
draggable
|
||||
@select="handleStepSelect"
|
||||
@expand="handleStepExpand"
|
||||
@more-actions-close="() => setFocusNodeKey('')"
|
||||
@more-action-select="handleStepMoreActionSelect"
|
||||
>
|
||||
<template #title="step">
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
|
@ -35,16 +39,18 @@
|
|||
</div>
|
||||
<div class="step-node-content">
|
||||
<!-- 步骤展开折叠按钮 -->
|
||||
<div
|
||||
<a-tooltip
|
||||
v-if="step.children?.length > 0"
|
||||
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"
|
||||
:content="t('apiScenario.expandStepTip', { count: step.children.length })"
|
||||
>
|
||||
<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="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
|
||||
<MsIcon
|
||||
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
|
||||
:size="14"
|
||||
/>
|
||||
{{ step.children?.length || 0 }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<div class="mr-[8px] flex items-center gap-[8px]">
|
||||
<!-- 步骤启用/禁用 -->
|
||||
<a-switch
|
||||
|
@ -62,46 +68,105 @@
|
|||
</div>
|
||||
<!-- 步骤类型 -->
|
||||
<stepType :type="step.type" />
|
||||
<apiMethodName v-if="checkStepIsApi(step)" :method="step.method" />
|
||||
<!-- 步骤名称 -->
|
||||
<div v-if="checkStepIsApi(step)" class="relative flex flex-1 items-center">
|
||||
<div
|
||||
v-if="step.id === showStepNameEditInputStepId"
|
||||
class="absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
@click.stop
|
||||
>
|
||||
<a-input
|
||||
:id="step.id"
|
||||
v-model:model-value="tempStepName"
|
||||
:placeholder="t('apiScenario.pleaseInputStepName')"
|
||||
:max-length="255"
|
||||
size="small"
|
||||
@press-enter="applyStepChange(step)"
|
||||
@blur="applyStepChange(step)"
|
||||
/>
|
||||
</div>
|
||||
<a-tooltip :content="step.name">
|
||||
<div class="step-name-container">
|
||||
<div class="one-line-text mr-[4px] max-w-[250px] font-medium text-[var(--color-text-1)]">
|
||||
{{ step.name }}
|
||||
</div>
|
||||
<MsIcon
|
||||
type="icon-icon_edit_outlined"
|
||||
class="edit-script-name-icon"
|
||||
@click.stop="handleStepNameClick(step)"
|
||||
<!-- 步骤整体内容 -->
|
||||
<div class="relative flex flex-1 items-center gap-[4px]">
|
||||
<!-- 步骤差异内容,按步骤类型展示不同组件 -->
|
||||
<component :is="getStepContent(step)" :data="getStepContentData(step)" />
|
||||
<!-- API、CASE、场景步骤名称 -->
|
||||
<template v-if="checkStepIsApi(step)">
|
||||
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
|
||||
<div
|
||||
v-if="step.id === showStepNameEditInputStepId"
|
||||
class="absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
@click.stop
|
||||
>
|
||||
<a-input
|
||||
:id="step.id"
|
||||
v-model:model-value="tempStepName"
|
||||
:placeholder="t('apiScenario.pleaseInputStepName')"
|
||||
:max-length="255"
|
||||
size="small"
|
||||
@press-enter="applyStepChange(step)"
|
||||
@blur="applyStepChange(step)"
|
||||
/>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="step.name">
|
||||
<div class="step-name-container">
|
||||
<div class="one-line-text mr-[4px] max-w-[250px] font-medium text-[var(--color-text-1)]">
|
||||
{{ step.name }}
|
||||
</div>
|
||||
<MsIcon
|
||||
type="icon-icon_edit_outlined"
|
||||
class="edit-script-name-icon"
|
||||
@click.stop="handleStepNameClick(step)"
|
||||
/>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 步骤内容,按步骤类型展示不同组件 -->
|
||||
<component :is="getStepContent(step)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra="step">
|
||||
<MsButton :id="step.id" 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>
|
||||
<a-trigger
|
||||
trigger="click"
|
||||
class="arco-trigger-menu absolute"
|
||||
content-class="w-[160px]"
|
||||
position="br"
|
||||
@popup-visible-change="handleActionTriggerChange($event, step)"
|
||||
>
|
||||
<MsButton
|
||||
:id="step.id"
|
||||
type="icon"
|
||||
class="ms-tree-node-extra__btn !mr-[4px]"
|
||||
@click="setFocusNodeKey(step.id)"
|
||||
>
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<actionDropdown
|
||||
v-model:visible="step.actionDropdownVisible"
|
||||
position="br"
|
||||
class="scenario-action-dropdown"
|
||||
:popup-translate="[-7, -10]"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType, step)"
|
||||
>
|
||||
<span></span>
|
||||
</actionDropdown>
|
||||
<div class="arco-trigger-menu-inner">
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'addChildStep' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'addChildStep')"
|
||||
>
|
||||
<icon-plus size="12" />
|
||||
{{ t('apiScenario.addChildStep') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'insertBefore' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'insertBefore')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertBefore') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'insertAfter' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'insertAfter')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertAfter') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-trigger>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<executeStatus v-if="step.status" :status="step.status" size="small" />
|
||||
|
@ -114,7 +179,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</MsTree>
|
||||
</div>
|
||||
</a-spin>
|
||||
<actionDropdown
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
|
||||
|
@ -133,6 +198,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
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';
|
||||
|
@ -153,7 +220,8 @@
|
|||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { findNodeByKey } from '@/utils';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { deleteNode, findNodeByKey, getGenerateId, insertNode, mapTree } from '@/utils';
|
||||
|
||||
import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
|
@ -168,11 +236,16 @@
|
|||
description: string;
|
||||
method?: RequestMethods;
|
||||
status?: ScenarioExecuteStatus;
|
||||
projectId?: string;
|
||||
num?: number; // 详情或者引用的类型才有
|
||||
// 引用类型专有字段
|
||||
belongProjectId?: string;
|
||||
belongProjectName?: string;
|
||||
children?: ScenarioStepItem[];
|
||||
// 页面渲染以及交互需要字段
|
||||
// renderId: string; // 渲染id
|
||||
checked: boolean; // 是否选中
|
||||
expanded: boolean; // 是否展开
|
||||
actionDropdownVisible?: boolean; // 是否展示操作下拉
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -180,6 +253,7 @@
|
|||
expandAll?: boolean;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const steps = defineModel<ScenarioStepItem[]>('steps', {
|
||||
|
@ -189,27 +263,10 @@
|
|||
required: true,
|
||||
});
|
||||
|
||||
const selectedKeys = ref<string[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const loading = ref(false);
|
||||
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 getStepContent(step: ScenarioStepItem) {
|
||||
switch (step.type) {
|
||||
|
@ -232,12 +289,138 @@
|
|||
}
|
||||
}
|
||||
|
||||
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||
focusStepKey.value = node.id || '';
|
||||
function getStepContentData(step: ScenarioStepItem) {
|
||||
switch (step.type) {
|
||||
case ScenarioStepType.QUOTE_API:
|
||||
case ScenarioStepType.QUOTE_CASE:
|
||||
case ScenarioStepType.QUOTE_SCENARIO:
|
||||
return {
|
||||
id: step.id,
|
||||
belongProjectId: step.belongProjectId,
|
||||
belongProjectName: step.belongProjectName,
|
||||
num: step.num,
|
||||
name: step.name,
|
||||
type: step.type,
|
||||
currentProjectId: appStore.currentProjectId,
|
||||
};
|
||||
case ScenarioStepType.CUSTOM_API:
|
||||
return {};
|
||||
case ScenarioStepType.LOOP_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.CONDITION_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.ONLY_ONCE_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.WAIT_TIME:
|
||||
return {};
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setFocusNodeKey(id: string) {
|
||||
focusStepKey.value = id || '';
|
||||
}
|
||||
|
||||
function checkStepIsApi(step: ScenarioStepItem) {
|
||||
return [ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API, ScenarioStepType.CUSTOM_API].includes(step.type);
|
||||
return [
|
||||
ScenarioStepType.QUOTE_API,
|
||||
ScenarioStepType.COPY_API,
|
||||
ScenarioStepType.QUOTE_CASE,
|
||||
ScenarioStepType.COPY_CASE,
|
||||
ScenarioStepType.CUSTOM_API,
|
||||
].includes(step.type);
|
||||
}
|
||||
|
||||
function checkStepShowMethod(step: ScenarioStepItem) {
|
||||
return [
|
||||
ScenarioStepType.QUOTE_API,
|
||||
ScenarioStepType.COPY_API,
|
||||
ScenarioStepType.QUOTE_CASE,
|
||||
ScenarioStepType.COPY_CASE,
|
||||
ScenarioStepType.CUSTOM_API,
|
||||
ScenarioStepType.QUOTE_SCENARIO,
|
||||
ScenarioStepType.COPY_SCENARIO,
|
||||
].includes(step.type);
|
||||
}
|
||||
|
||||
const activeAction = ref('');
|
||||
function handleTriggerActionClick(step: ScenarioStepItem, action: string) {
|
||||
step.actionDropdownVisible = true;
|
||||
activeAction.value = action;
|
||||
}
|
||||
|
||||
function handleActionTriggerChange(val: boolean, step: ScenarioStepItem) {
|
||||
if (!val) {
|
||||
activeAction.value = '';
|
||||
step.actionDropdownVisible = false;
|
||||
setFocusNodeKey('');
|
||||
}
|
||||
}
|
||||
|
||||
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 handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||
switch (item.eventTag) {
|
||||
case 'execute':
|
||||
console.log('执行步骤', node);
|
||||
break;
|
||||
case 'copy':
|
||||
const id = getGenerateId();
|
||||
insertNode<ScenarioStepItem>(
|
||||
steps.value,
|
||||
node.id,
|
||||
{
|
||||
...cloneDeep(
|
||||
mapTree<ScenarioStepItem>(node, (childNode) => {
|
||||
const childId = getGenerateId();
|
||||
if (selectedKeys.value.includes(node.id)) {
|
||||
// 复制一个已选中的节点,需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(childId);
|
||||
}
|
||||
return {
|
||||
...childNode,
|
||||
id: childId,
|
||||
};
|
||||
})[0]
|
||||
),
|
||||
name: `copy-${node.name}`,
|
||||
id,
|
||||
},
|
||||
'after',
|
||||
'id'
|
||||
);
|
||||
if (selectedKeys.value.includes(node.id)) {
|
||||
// 复制一个已选中的节点,需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(id);
|
||||
}
|
||||
break;
|
||||
case 'config':
|
||||
console.log('config', node);
|
||||
break;
|
||||
case 'delete':
|
||||
deleteNode(steps.value, node.id, 'id');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function checkAll(val: boolean) {
|
||||
|
@ -276,6 +459,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleStepSelect(_selectedKeys: Array<string | number>, node: MsTreeNodeData) {
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(node.children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
selectedKeys.value = [node.id, ...offspringIds];
|
||||
}
|
||||
|
||||
function executeStep(node: MsTreeNodeData) {
|
||||
console.log('执行步骤', node);
|
||||
}
|
||||
|
@ -284,7 +476,7 @@
|
|||
const customApiDrawerVisible = ref(false);
|
||||
const scriptOperationDrawerVisible = ref(false);
|
||||
|
||||
function handleActionSelect(val: ScenarioAddStepActionType) {
|
||||
function handleActionSelect(val: ScenarioAddStepActionType, step?: ScenarioStepItem) {
|
||||
switch (val) {
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
importApiDrawerVisible.value = true;
|
||||
|
@ -338,6 +530,9 @@
|
|||
default:
|
||||
break;
|
||||
}
|
||||
if (step) {
|
||||
document.getElementById(step.id.toString())?.click();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
@ -345,6 +540,13 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.step-tree-active-action {
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.add-step-btn {
|
||||
@apply bg-white;
|
||||
|
|
|
@ -159,5 +159,13 @@
|
|||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
@apply pt-0;
|
||||
|
||||
height: calc(100% - 49px);
|
||||
.arco-tabs-content-list {
|
||||
@apply h-full;
|
||||
.arco-tabs-pane {
|
||||
@apply h-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -101,6 +101,13 @@ export default {
|
|||
'apiScenario.scenarioConfig': '场景配置',
|
||||
'apiScenario.noMatchStep': '暂无匹配的步骤数据',
|
||||
'apiScenario.pleaseInputStepName': '请输入步骤名称',
|
||||
'apiScenario.belongProject': '所属项目',
|
||||
'apiScenario.detailName': '名称',
|
||||
'apiScenario.crossProject': '跨项目',
|
||||
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
|
||||
'apiScenario.addChildStep': '添加子步骤',
|
||||
'apiScenario.insertBefore': '在之前插入步骤',
|
||||
'apiScenario.insertAfter': '在之后插入步骤',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
Loading…
Reference in New Issue