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

This commit is contained in:
baiqi 2024-03-18 21:07:26 +08:00 committed by Craftsman
parent 393bcabc3e
commit 488a93ea2c
12 changed files with 493 additions and 86 deletions

View File

@ -167,11 +167,11 @@
</a-form> </a-form>
<div <div
v-if="paramSettingType === 'mock' && paramForm.type !== ''" 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> <div class="break-all text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div>
<a-spin :loading="previewLoading" class="flex gap-[8px]"> <a-spin :loading="previewLoading" class="flex flex-1 flex-wrap gap-[8px]">
<div class="text-[var(--color-text-1)]">{{ paramPreview }}</div> <div class="param-preview">{{ paramPreview }}</div>
<MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton> <MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton>
</a-spin> </a-spin>
</div> </div>
@ -625,6 +625,13 @@
overflow-y: auto; overflow-y: auto;
margin-right: -6px; margin-right: -6px;
max-height: 400px; 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);
}
} }
} }
} }

View File

@ -24,13 +24,15 @@
:position="props.titleTooltipPosition" :position="props.titleTooltipPosition"
:disabled="props.disabledTitleTooltip" :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> </a-tooltip>
</template> </template>
<template v-if="$slots['drag-icon']" #drag-icon="_props"> <template v-if="$slots['drag-icon']" #drag-icon="_props">
<slot name="title" v-bind="_props"></slot> <slot name="title" v-bind="_props"></slot>
</template> </template>
<template v-if="$slots['extra']" #extra="_props"> <template v-if="$slots['extra'] || props.nodeMoreActions" #extra="_props">
<div <div
v-if="_props.hideMoreAction !== true" v-if="_props.hideMoreAction !== true"
:class="[ :class="[
@ -91,6 +93,7 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
keyword?: string; // keyword?: string; //
titleClass?: string; //
searchDebounce?: number; // ms searchDebounce?: number; // ms
draggable?: boolean; // draggable?: boolean; //
blockNode?: boolean; // blockNode?: boolean; //
@ -509,8 +512,11 @@
.arco-tree-node-title { .arco-tree-node-title {
font-weight: 500 !important; font-weight: 500 !important;
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
* { .ms-tree-node-title {
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
* {
color: rgb(var(--primary-5));
}
} }
} }
} }

View File

@ -33,7 +33,7 @@
type?: TagType; // tag type?: TagType; // tag
size?: Size; // tag size?: Size; // tag
theme?: Theme; // tag theme?: Theme; // tag
selfStyle?: any; // selfStyle?: Record<string, any>; //
width?: number; // tag,max-width width?: number; // tag,max-width
maxWidth?: string; maxWidth?: string;
noMargin?: boolean; // tag noMargin?: boolean; // tag

View File

@ -345,6 +345,68 @@ export function findNodePathByKey<T>(
return null; 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 * @param targetMap

View File

@ -1245,6 +1245,8 @@
/** /**
* 保存请求 * 保存请求
* @param fullParams 保存时传入的参数
* @param silence 是否静默保存接口定义另存为用例时要先静默保存接口
*/ */
async function realSave(fullParams?: Record<string, any>, silence?: boolean) { async function realSave(fullParams?: Record<string, any>, silence?: boolean) {
try { try {
@ -1278,6 +1280,7 @@
requestVModel.value.label = res.name; requestVModel.value.label = res.name;
requestVModel.value.url = res.path; requestVModel.value.url = res.path;
requestVModel.value.path = res.path; requestVModel.value.path = res.path;
requestVModel.value.moduleId = res.moduleId;
if (!props.isDefinition) { if (!props.isDefinition) {
saveModalVisible.value = false; saveModalVisible.value = false;
} }

View File

@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<a-divider class="my-[8px]" /> <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 <MsTree
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"

View File

@ -1,5 +1,11 @@
<template> <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> <slot></slot>
<template #content> <template #content>
<a-dgroup :title="t('apiScenario.requestScenario')"> <a-dgroup :title="t('apiScenario.requestScenario')">
@ -35,18 +41,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { TriggerPopupTranslate } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ScenarioAddStepActionType } from '@/enums/apiEnum'; 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<{ const emit = defineEmits<{
(e: 'select', val: ScenarioAddStepActionType): void; (e: 'select', val: ScenarioAddStepActionType): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
default: false,
});
function openTutorial() { function openTutorial() {
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank'); window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col gap-[16px]"> <div class="flex h-full flex-col gap-[16px]">
<div class="action-line"> <div class="action-line">
<div class="action-group"> <div class="action-group">
<a-checkbox <a-checkbox
@ -79,7 +79,7 @@
</a-button> </a-button>
</div> </div>
</div> </div>
<div> <div class="h-[calc(100%-48px)]">
<stepTree <stepTree
ref="stepTreeRef" ref="stepTreeRef"
v-model:steps="stepInfo.steps" v-model:steps="stepInfo.steps"
@ -99,6 +99,7 @@
import stepTree, { ScenarioStepItem } from './stepTree.vue'; import stepTree, { ScenarioStepItem } from './stepTree.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum'; import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
@ -114,6 +115,7 @@
isNew?: boolean; // isNew?: boolean; //
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const checkedAll = ref(false); // const checkedAll = ref(false); //
@ -127,6 +129,7 @@
steps: [ steps: [
{ {
id: 1, id: 1,
num: 10086,
order: 1, order: 1,
checked: false, checked: false,
expanded: false, expanded: false,
@ -135,6 +138,9 @@
name: 'API1', name: 'API1',
description: 'API1描述', description: 'API1描述',
method: RequestMethods.GET, method: RequestMethods.GET,
belongProjectId: appStore.currentProjectId,
belongProjectName: '项目名称',
actionDropdownVisible: false,
children: [ children: [
{ {
id: 11, id: 11,
@ -146,6 +152,10 @@
name: 'API11', name: 'API11',
description: 'API11描述', description: 'API11描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
num: 100861,
belongProjectId: '989d23d23d',
belongProjectName: '项目名称1',
actionDropdownVisible: false,
}, },
{ {
id: 12, id: 12,
@ -157,6 +167,10 @@
name: 'API12', name: 'API12',
description: 'API12描述', description: 'API12描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
num: 100862,
belongProjectId: '989d23d23d',
belongProjectName: '项目名称2',
actionDropdownVisible: false,
}, },
], ],
}, },
@ -170,6 +184,7 @@
name: 'API1', name: 'API1',
description: 'API1描述', description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
actionDropdownVisible: false,
}, },
{ {
id: 3, id: 3,
@ -181,6 +196,7 @@
name: 'API1', name: 'API1',
description: 'API1描述', description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
actionDropdownVisible: false,
}, },
], ],
executeTime: '', executeTime: '',

View File

@ -1,7 +1,86 @@
<template> <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> </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>

View File

@ -1,8 +1,9 @@
<template> <template>
<div class="flex flex-col gap-[16px]"> <div class="flex h-full flex-col gap-[16px]">
<div class="max-h-[calc(100vh-305px)]"> <a-spin class="max-h-[calc(100%-46px)] w-full" :loading="loading">
<MsTree <MsTree
ref="treeRef" ref="treeRef"
v-model:selected-keys="selectedKeys"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:focus-node-key="focusStepKey" v-model:focus-node-key="focusStepKey"
v-model:data="steps" v-model:data="steps"
@ -10,20 +11,23 @@
:expand-all="props.expandAll" :expand-all="props.expandAll"
:node-more-actions="stepMoreActions" :node-more-actions="stepMoreActions"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
:selectable="false"
:virtual-list-props="{ :virtual-list-props="{
height: '100%', height: '100%',
threshold: 200, threshold: 20,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15, // 10 padding
}" }"
title-class="step-tree-node-title"
node-highlight-background-color="var(--color-text-n9)" node-highlight-background-color="var(--color-text-n9)"
action-on-node-click="expand" action-on-node-click="expand"
disabled-title-tooltip disabled-title-tooltip
checkable checkable
block-node block-node
draggable draggable
@select="handleStepSelect"
@expand="handleStepExpand" @expand="handleStepExpand"
@more-actions-close="() => setFocusNodeKey('')"
@more-action-select="handleStepMoreActionSelect"
> >
<template #title="step"> <template #title="step">
<div class="flex w-full items-center gap-[8px]"> <div class="flex w-full items-center gap-[8px]">
@ -35,16 +39,18 @@
</div> </div>
<div class="step-node-content"> <div class="step-node-content">
<!-- 步骤展开折叠按钮 --> <!-- 步骤展开折叠按钮 -->
<div <a-tooltip
v-if="step.children?.length > 0" 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 <div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" <MsIcon
:size="14" :type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
/> :size="14"
{{ step.children?.length || 0 }} />
</div> {{ step.children?.length || 0 }}
</div>
</a-tooltip>
<div class="mr-[8px] flex items-center gap-[8px]"> <div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 --> <!-- 步骤启用/禁用 -->
<a-switch <a-switch
@ -62,46 +68,105 @@
</div> </div>
<!-- 步骤类型 --> <!-- 步骤类型 -->
<stepType :type="step.type" /> <stepType :type="step.type" />
<apiMethodName v-if="checkStepIsApi(step)" :method="step.method" /> <!-- 步骤整体内容 -->
<!-- 步骤名称 --> <div class="relative flex flex-1 items-center gap-[4px]">
<div v-if="checkStepIsApi(step)" class="relative flex flex-1 items-center"> <!-- 步骤差异内容按步骤类型展示不同组件 -->
<div <component :is="getStepContent(step)" :data="getStepContentData(step)" />
v-if="step.id === showStepNameEditInputStepId" <!-- APICASE场景步骤名称 -->
class="absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]" <template v-if="checkStepIsApi(step)">
@click.stop <apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
> <div
<a-input v-if="step.id === showStepNameEditInputStepId"
:id="step.id" class="absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
v-model:model-value="tempStepName" @click.stop
:placeholder="t('apiScenario.pleaseInputStepName')" >
:max-length="255" <a-input
size="small" :id="step.id"
@press-enter="applyStepChange(step)" v-model:model-value="tempStepName"
@blur="applyStepChange(step)" :placeholder="t('apiScenario.pleaseInputStepName')"
/> :max-length="255"
</div> size="small"
<a-tooltip :content="step.name"> @press-enter="applyStepChange(step)"
<div class="step-name-container"> @blur="applyStepChange(step)"
<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> </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> </div>
<!-- 步骤内容按步骤类型展示不同组件 -->
<component :is="getStepContent(step)" />
</div> </div>
</div> </div>
</template> </template>
<template #extra="step"> <template #extra="step">
<MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="setFocusNodeKey(step)"> <a-trigger
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" /> trigger="click"
</MsButton> 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>
<template #extraEnd="step"> <template #extraEnd="step">
<executeStatus v-if="step.status" :status="step.status" size="small" /> <executeStatus v-if="step.status" :status="step.status" size="small" />
@ -114,7 +179,7 @@
</div> </div>
</template> </template>
</MsTree> </MsTree>
</div> </a-spin>
<actionDropdown <actionDropdown
class="scenario-action-dropdown" class="scenario-action-dropdown"
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)" @select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
@ -133,6 +198,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
@ -153,7 +220,8 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { useI18n } from '@/hooks/useI18n'; 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'; import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
@ -168,11 +236,16 @@
description: string; description: string;
method?: RequestMethods; method?: RequestMethods;
status?: ScenarioExecuteStatus; status?: ScenarioExecuteStatus;
projectId?: string; num?: number; //
//
belongProjectId?: string;
belongProjectName?: string;
children?: ScenarioStepItem[]; children?: ScenarioStepItem[];
// //
// renderId: string; // id
checked: boolean; // checked: boolean; //
expanded: boolean; // expanded: boolean; //
actionDropdownVisible?: boolean; //
} }
const props = defineProps<{ const props = defineProps<{
@ -180,6 +253,7 @@
expandAll?: boolean; expandAll?: boolean;
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const steps = defineModel<ScenarioStepItem[]>('steps', { const steps = defineModel<ScenarioStepItem[]>('steps', {
@ -189,27 +263,10 @@
required: true, required: true,
}); });
const selectedKeys = ref<string[]>([]); //
const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>(); const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string>(''); // key 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) { function getStepContent(step: ScenarioStepItem) {
switch (step.type) { switch (step.type) {
@ -232,12 +289,138 @@
} }
} }
function setFocusNodeKey(node: MsTreeNodeData) { function getStepContentData(step: ScenarioStepItem) {
focusStepKey.value = node.id || ''; 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) { 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) { 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) { function executeStep(node: MsTreeNodeData) {
console.log('执行步骤', node); console.log('执行步骤', node);
} }
@ -284,7 +476,7 @@
const customApiDrawerVisible = ref(false); const customApiDrawerVisible = ref(false);
const scriptOperationDrawerVisible = ref(false); const scriptOperationDrawerVisible = ref(false);
function handleActionSelect(val: ScenarioAddStepActionType) { function handleActionSelect(val: ScenarioAddStepActionType, step?: ScenarioStepItem) {
switch (val) { switch (val) {
case ScenarioAddStepActionType.IMPORT_SYSTEM_API: case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
importApiDrawerVisible.value = true; importApiDrawerVisible.value = true;
@ -338,6 +530,9 @@
default: default:
break; break;
} }
if (step) {
document.getElementById(step.id.toString())?.click();
}
} }
defineExpose({ defineExpose({
@ -345,6 +540,13 @@
}); });
</script> </script>
<style lang="less">
.step-tree-active-action {
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
}
</style>
<style lang="less" scoped> <style lang="less" scoped>
.add-step-btn { .add-step-btn {
@apply bg-white; @apply bg-white;

View File

@ -159,5 +159,13 @@
} }
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
@apply pt-0; @apply pt-0;
height: calc(100% - 49px);
.arco-tabs-content-list {
@apply h-full;
.arco-tabs-pane {
@apply h-full;
}
}
} }
</style> </style>

View File

@ -101,6 +101,13 @@ export default {
'apiScenario.scenarioConfig': '场景配置', 'apiScenario.scenarioConfig': '场景配置',
'apiScenario.noMatchStep': '暂无匹配的步骤数据', 'apiScenario.noMatchStep': '暂无匹配的步骤数据',
'apiScenario.pleaseInputStepName': '请输入步骤名称', 'apiScenario.pleaseInputStepName': '请输入步骤名称',
'apiScenario.belongProject': '所属项目',
'apiScenario.detailName': '名称',
'apiScenario.crossProject': '跨项目',
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
'apiScenario.addChildStep': '添加子步骤',
'apiScenario.insertBefore': '在之前插入步骤',
'apiScenario.insertAfter': '在之后插入步骤',
// 执行历史 // 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索', 'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号', 'apiScenario.executeHistory.num': '序号',