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

This commit is contained in:
baiqi 2024-03-17 17:12:59 +08:00 committed by Craftsman
parent b99d7a60fa
commit f5dc90ffd8
18 changed files with 474 additions and 134 deletions

View File

@ -443,16 +443,16 @@
height: 14px; height: 14px;
} }
} }
margin-right: 24px; margin-right: 24px;
} }
.arco-form-item{ .arco-form-item {
margin-bottom: 16px; margin-bottom: 16px;
} }
.arco-icon-hover.arco-radio-icon-hover::before { .arco-icon-hover.arco-radio-icon-hover::before {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
.arco-radio-checked:not(.arco-radio-disabled) { .arco-radio-checked:not(.arco-radio-disabled) {
.arco-radio-icon { .arco-radio-icon {
@apply !bg-white; @apply !bg-white;
@ -653,6 +653,12 @@
.arco-switch { .arco-switch {
margin-left: 2px; // 避免开关圆形左边被遮挡 margin-left: 2px; // 避免开关圆形左边被遮挡
} }
.arco-switch-type-circle {
background-color: var(--color-text-brand) !important;
}
.arco-switch-type-circle.arco-switch-checked {
background-color: rgb(var(--primary-5)) !important;
}
.arco-switch-type-line.arco-switch-small { .arco-switch-type-line.arco-switch-small {
.arco-switch-handle { .arco-switch-handle {
width: 14px; width: 14px;
@ -661,7 +667,7 @@
} }
.arco-switch-type-line.arco-switch-small.arco-switch-checked { .arco-switch-type-line.arco-switch-small.arco-switch-checked {
.arco-switch-handle { .arco-switch-handle {
left: calc(100% - 14px - 0px); left: calc(100% - 14px);
} }
} }

View File

@ -1,13 +1,13 @@
<template> <template>
<div ref="treeContainerRef" :class="['ms-tree-container', containerStatusClass]"> <div ref="treeContainerRef" :class="['ms-tree-container', containerStatusClass]">
<a-tree <a-tree
v-show="treeData.length > 0" v-show="data.length > 0"
v-bind="props" v-bind="props"
ref="treeRef" ref="treeRef"
v-model:expanded-keys="expandedKeys" v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
:data="treeData" :data="data"
class="ms-tree" class="ms-tree"
:allow-drop="handleAllowDrop" :allow-drop="handleAllowDrop"
@drag-start="onDragStart" @drag-start="onDragStart"
@ -15,6 +15,7 @@
@drop="onDrop" @drop="onDrop"
@select="select" @select="select"
@check="checked" @check="checked"
@expand="expand"
> >
<template v-if="$slots['title']" #title="_props"> <template v-if="$slots['title']" #title="_props">
<a-tooltip <a-tooltip
@ -64,7 +65,7 @@
</a-tree> </a-tree>
<slot name="empty"> <slot name="empty">
<div <div
v-show="treeData.length === 0 && props.emptyText" v-show="data.length === 0 && props.emptyText"
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]" class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
> >
{{ props.emptyText }} {{ props.emptyText }}
@ -74,8 +75,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue'; import { nextTick, onBeforeMount, Ref, ref, watch } from 'vue';
import { debounce } from 'lodash-es'; import { cloneDeep, debounce } 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';
@ -83,14 +84,12 @@
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import useContainerShadow from '@/hooks/useContainerShadow'; import useContainerShadow from '@/hooks/useContainerShadow';
import { mapTree } from '@/utils/index';
import type { MsTreeFieldNames, MsTreeNodeData, MsTreeSelectedData } from './types'; import type { MsTreeExpandedData, MsTreeFieldNames, MsTreeNodeData, MsTreeSelectedData } from './types';
import { VirtualListProps } from '@arco-design/web-vue/es/_components/virtual-list-v2/interface'; import { VirtualListProps } from '@arco-design/web-vue/es/_components/virtual-list-v2/interface';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
data: MsTreeNodeData[];
keyword?: string; // keyword?: string; //
searchDebounce?: number; // ms searchDebounce?: number; // ms
draggable?: boolean; // draggable?: boolean; //
@ -107,6 +106,8 @@
checkedStrategy?: 'all' | 'parent' | 'child'; // checkedStrategy?: 'all' | 'parent' | 'child'; //
virtualListProps?: VirtualListProps; // virtualListProps?: VirtualListProps; //
disabledTitleTooltip?: boolean; // tooltip disabledTitleTooltip?: boolean; // tooltip
actionOnNodeClick?: 'expand'; //
nodeHighlightBackgroundColor?: string; //
titleTooltipPosition?: titleTooltipPosition?:
| 'top' | 'top'
| 'tl' | 'tl'
@ -151,8 +152,12 @@
(e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void; (e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void;
(e: 'moreActionsClose'): void; (e: 'moreActionsClose'): void;
(e: 'check', val: Array<string | number>): void; (e: 'check', val: Array<string | number>): void;
(e: 'expand', node: MsTreeExpandedData): void;
}>(); }>();
const data = defineModel<MsTreeNodeData[]>('data', {
required: true,
});
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', { const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
default: [], default: [],
}); });
@ -172,25 +177,8 @@
overHeight: 32, overHeight: 32,
containerClassName: 'ms-tree-container', containerClassName: 'ms-tree-container',
}); });
const originalTreeData = ref<MsTreeNodeData[]>([]);
function init(isFirstInit = false) { 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;
// });
nextTick(() => { nextTick(() => {
if (isFirstInit) { if (isFirstInit) {
if (props.defaultExpandAll) { if (props.defaultExpandAll) {
@ -212,10 +200,18 @@
init(true); init(true);
}); });
const originTreeData = ref<MsTreeNodeData[]>([]); //
watch( watch(
() => props.data, () => data.value,
() => { (val) => {
init(); if (!props.keyword) {
originTreeData.value = cloneDeep(val);
}
},
{
deep: true,
immediate: true,
} }
); );
@ -224,16 +220,17 @@
* @param keyword 搜索关键字 * @param keyword 搜索关键字
*/ */
function searchData(keyword: string) { function searchData(keyword: string) {
const search = (data: MsTreeNodeData[]) => { const search = (_data: MsTreeNodeData[]) => {
const result: MsTreeNodeData[] = []; const result: MsTreeNodeData[] = [];
data.forEach((item) => { _data.forEach((item) => {
if (item[props.fieldNames.title].toLowerCase().indexOf(keyword.toLowerCase()) > -1) { if (item[props.fieldNames.title].toLowerCase().indexOf(keyword.toLowerCase()) > -1) {
result.push({ ...item }); result.push({ ...item, expanded: true });
} else if (item[props.fieldNames.children]) { } else if (item[props.fieldNames.children]) {
const filterData = search(item[props.fieldNames.children]); const filterData = search(item[props.fieldNames.children]);
if (filterData.length) { if (filterData.length) {
result.push({ result.push({
...item, ...item,
expanded: true,
[props.fieldNames.children]: filterData, [props.fieldNames.children]: filterData,
}); });
} }
@ -243,25 +240,26 @@
return result; return result;
}; };
return search(originalTreeData.value); return search(originTreeData.value);
} }
const treeData = ref<MsTreeNodeData[]>([]);
// //
const updateDebouncedSearch = debounce(() => { const updateDebouncedSearch = debounce(() => {
if (props.keyword) { if (props.keyword) {
treeData.value = searchData(props.keyword); data.value = searchData(props.keyword);
} }
}, props.searchDebounce); }, props.searchDebounce);
watchEffect(() => { watch(
if (!props.keyword) { () => props.keyword,
treeData.value = originalTreeData.value; (val) => {
} else { if (!val) {
updateDebouncedSearch(); data.value = cloneDeep(originTreeData.value);
} else {
updateDebouncedSearch();
}
} }
}); );
function loop( function loop(
_data: MsTreeNodeData[], _data: MsTreeNodeData[],
@ -309,13 +307,13 @@
dropNode: MsTreeNodeData; // dropNode: MsTreeNodeData; //
dropPosition: number; // -1 1 0 dropPosition: number; // -1 1 0
}) { }) {
loop(originalTreeData.value, dragNode.key, (item, index, arr) => { loop(data.value, dragNode.key, (item, index, arr) => {
arr.splice(index, 1); arr.splice(index, 1);
}); });
if (dropPosition === 0) { if (dropPosition === 0) {
// //
loop(originalTreeData.value, dropNode.key, (item) => { loop(data.value, dropNode.key, (item) => {
item.children = item.children || []; item.children = item.children || [];
item.children.push(dragNode); item.children.push(dragNode);
}); });
@ -325,18 +323,18 @@
} }
} else { } else {
// //
loop(originalTreeData.value, dropNode.key, (item, index, arr) => { loop(data.value, dropNode.key, (item, index, arr) => {
arr.splice(dropPosition < 0 ? index : index + 1, 0, dragNode); arr.splice(dropPosition < 0 ? index : index + 1, 0, dragNode);
}); });
} }
emit('drop', originalTreeData.value, dragNode, dropNode, dropPosition); emit('drop', data.value, dragNode, dropNode, dropPosition);
} }
/** /**
* 处理树节点选中非复选框 * 处理树节点选中非复选框
*/ */
function select(_selectedKeys: Array<string | number>, data: MsTreeSelectedData) { function select(_selectedKeys: Array<string | number>, _data: MsTreeSelectedData) {
emit('select', _selectedKeys, data.selectedNodes[0]); emit('select', _selectedKeys, _data.selectedNodes[0]);
} }
function checked(_checkedKeys: Array<string | number>) { function checked(_checkedKeys: Array<string | number>) {
@ -351,7 +349,7 @@
if (val?.toString() !== '') { if (val?.toString() !== '') {
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`); focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
if (focusEl.value) { if (focusEl.value) {
focusEl.value.style.backgroundColor = 'rgb(var(--primary-1))'; focusEl.value.style.backgroundColor = props.nodeHighlightBackgroundColor || 'rgb(var(--primary-1))';
} }
} else if (focusEl.value) { } else if (focusEl.value) {
focusEl.value.style.backgroundColor = ''; focusEl.value.style.backgroundColor = '';
@ -380,6 +378,10 @@
} }
); );
function expand(expandKeys: Array<string | number>, node: MsTreeExpandedData) {
emit('expand', node);
}
function checkAll(val: boolean) { function checkAll(val: boolean) {
treeRef.value?.checkAll(val); treeRef.value?.checkAll(val);
} }

View File

@ -10,6 +10,7 @@ export interface MsTreeFieldNames extends TreeFieldNames {
export type MsTreeNodeData = { export type MsTreeNodeData = {
hideMoreAction?: boolean; // 隐藏更多操作 hideMoreAction?: boolean; // 隐藏更多操作
parentId?: string; parentId?: string;
expanded?: boolean; // 是否展开
[key: string]: any; [key: string]: any;
} & TreeNodeData; } & TreeNodeData;
@ -28,3 +29,10 @@ export interface MsTreeSelectedData {
node?: MsTreeNodeData; node?: MsTreeNodeData;
e?: Event; e?: Event;
} }
export interface MsTreeExpandedData {
expanded?: boolean;
expandedNodes: MsTreeNodeData[];
node?: MsTreeNodeData;
e?: Event;
}

View File

@ -268,7 +268,11 @@ export function filterTree<T>(
* @param customKey key * @param customKey key
* @returns /null * @returns /null
*/ */
export function findNodeByKey<T>(trees: TreeNode<T>[], targetKey: string, customKey = 'key'): TreeNode<T> | T | null { export function findNodeByKey<T>(
trees: TreeNode<T>[],
targetKey: string | number,
customKey = 'key'
): TreeNode<T> | T | null {
for (let i = 0; i < trees.length; i++) { for (let i = 0; i < trees.length; i++) {
const node = trees[i]; const node = trees[i];
if (node[customKey] === targetKey) { if (node[customKey] === targetKey) {

View File

@ -0,0 +1,23 @@
<template>
<MsDrawer
v-model:visible="visible"
:title="t('apiScenario.scriptOperation')"
:width="960"
no-content-padding
disabled-width-drag
>
waiting customApi
</MsDrawer>
</template>
<script setup lang="ts">
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,23 @@
<template>
<MsDrawer
v-model:visible="visible"
:title="t('apiScenario.scriptOperation')"
:width="960"
no-content-padding
disabled-width-drag
>
waiting scriptOperation
</MsDrawer>
</template>
<script setup lang="ts">
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
</script>
<style lang="less" scoped></style>

View File

@ -2,11 +2,11 @@
<div <div
class="rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]" class="rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{ :style="{
borderColor: status.color, borderColor: type.color,
color: status.color, color: type.color,
}" }"
> >
{{ status.label }} {{ type.label }}
</div> </div>
</template> </template>
@ -16,7 +16,7 @@
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
status: ScenarioStepType; type: ScenarioStepType;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -37,8 +37,8 @@
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' }, [ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
}; };
const status = computed(() => { const type = computed(() => {
const config = scenarioStepMap[props.status]; const config = scenarioStepMap[props.type];
return { return {
border: `1px solid ${config?.color}`, border: `1px solid ${config?.color}`,
color: config?.color, color: config?.color,

View File

@ -0,0 +1,7 @@
export const defaultStepItemCommon = {
checked: false,
expanded: false,
enabled: true,
};
export default {};

View File

@ -61,13 +61,11 @@
</div> </div>
</template> </template>
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto"> <div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
<a-input-search <a-input
v-model:model-value="keyword" v-model:model-value="keyword"
:placeholder="t('apiScenario.searchByName')" :placeholder="t('apiScenario.searchByName')"
allow-clear allow-clear
class="w-[200px]" class="w-[200px]"
@search="searchStep"
@press-enter="searchStep"
/> />
<a-button <a-button
v-if="!props.isNew" v-if="!props.isNew"
@ -82,7 +80,13 @@
</div> </div>
</div> </div>
<div> <div>
<stepTree ref="stepTreeRef" v-model:checked-keys="checkedKeys" :steps="stepInfo.steps" /> <stepTree
ref="stepTreeRef"
v-model:steps="stepInfo.steps"
v-model:checked-keys="checkedKeys"
v-model:stepKeyword="keyword"
:expand-all="isExpandAll"
/>
</div> </div>
</div> </div>
</template> </template>
@ -96,7 +100,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum'; import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
export interface ScenarioStepInfo { export interface ScenarioStepInfo {
id: string | number; id: string | number;
@ -125,16 +129,20 @@
id: 1, id: 1,
order: 1, order: 1,
checked: false, checked: false,
type: ScenarioStepType.CUSTOM_API, expanded: false,
enabled: true,
type: ScenarioStepType.QUOTE_API,
name: 'API1', name: 'API1',
description: 'API1描述', description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS, method: RequestMethods.GET,
children: [ children: [
{ {
id: 11, id: 11,
order: 1, order: 1,
checked: false, checked: false,
type: ScenarioStepType.CUSTOM_API, expanded: false,
enabled: true,
type: ScenarioStepType.QUOTE_CASE,
name: 'API11', name: 'API11',
description: 'API11描述', description: 'API11描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
@ -143,7 +151,9 @@
id: 12, id: 12,
order: 2, order: 2,
checked: false, checked: false,
type: ScenarioStepType.CUSTOM_API, expanded: false,
enabled: true,
type: ScenarioStepType.QUOTE_SCENARIO,
name: 'API12', name: 'API12',
description: 'API12描述', description: 'API12描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
@ -154,7 +164,20 @@
id: 2, id: 2,
order: 2, order: 2,
checked: false, checked: false,
type: ScenarioStepType.CUSTOM_API, expanded: false,
enabled: true,
type: ScenarioStepType.LOOP_CONTROL,
name: 'API1',
description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS,
},
{
id: 3,
order: 3,
checked: false,
expanded: false,
enabled: true,
type: ScenarioStepType.ONLY_ONCE_CONTROL,
name: 'API1', name: 'API1',
description: 'API1描述', description: 'API1描述',
status: ScenarioExecuteStatus.SUCCESS, status: ScenarioExecuteStatus.SUCCESS,
@ -215,10 +238,6 @@
function refreshStepInfo() { function refreshStepInfo() {
console.log('刷新步骤信息'); console.log('刷新步骤信息');
} }
function searchStep(val: string) {
stepInfo.value.steps = stepInfo.value.steps.filter((item) => item.name.includes(val));
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -0,0 +1,7 @@
<template>
<div>condition </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div>custom </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div>loop </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div>only once </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div>quote </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div>waitTime </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -1,50 +1,120 @@
<template> <template>
<div class="flex flex-col gap-[16px]"> <div class="flex flex-col gap-[16px]">
<MsTree <div class="max-h-[calc(100vh-305px)]">
ref="treeRef" <MsTree
v-model:checked-keys="checkedKeys" ref="treeRef"
v-model:focus-node-key="focusStepKey" v-model:checked-keys="checkedKeys"
:data="props.steps" v-model:focus-node-key="focusStepKey"
:node-more-actions="stepMoreActions" v-model:data="steps"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :keyword="props.stepKeyword"
:selectable="false" :expand-all="props.expandAll"
disabled-title-tooltip :node-more-actions="stepMoreActions"
checkable :field-names="{ title: 'name', key: 'id', children: 'children' }"
block-node :selectable="false"
draggable :virtual-list-props="{
> height: '100%',
<template #title="step"> threshold: 200,
<div class="flex items-center gap-[8px]"> fixedSize: true,
<div buffer: 15, // 10 padding
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white" }"
> node-highlight-background-color="var(--color-text-n9)"
{{ step.order }} action-on-node-click="expand"
</div> disabled-title-tooltip
<div class="step-node-first"> checkable
block-node
draggable
@expand="handleStepExpand"
>
<template #title="step">
<div class="flex w-full items-center gap-[8px]">
<!-- 步骤序号 -->
<div <div
v-show="step.children?.length > 0" class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"
@click.stop="toggleNodeExpand(step)"
> >
<MsIcon {{ step.order }}
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" </div>
:size="14" <div class="step-node-content">
/> <!-- 步骤展开折叠按钮 -->
{{ step.children?.length || 0 }} <div
v-if="step.children?.length > 0"
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>
<div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 -->
<a-switch
:default-checked="step.enabled"
size="small"
@click.stop="step.enabled = !step.enabled"
></a-switch>
<!-- 步骤执行 -->
<MsIcon
type="icon-icon_play-round_filled"
:size="18"
class="cursor-pointer text-[rgb(var(--link-6))]"
@click.stop="executeStep(step)"
/>
</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>
</a-tooltip>
</div>
<!-- 步骤内容按步骤类型展示不同组件 -->
<component :is="getStepContent(step)" />
</div> </div>
<div class="text-[var(--color-text-1)]">{{ step.name }}</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)">
<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)]" />
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" /> </MsButton>
</MsButton> </template>
</template> <template #extraEnd="step">
<template #extraEnd="step"> <executeStatus v-if="step.status" :status="step.status" size="small" />
<executeStatus :status="step.status" size="small" /> </template>
</template> <template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
</MsTree> <div
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-center text-[12px] leading-[16px] text-[var(--color-text-4)]"
>
{{ t('apiScenario.noMatchStep') }}
</div>
</template>
</MsTree>
</div>
<actionDropdown <actionDropdown
class="scenario-action-dropdown" class="scenario-action-dropdown"
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)" @select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
@ -57,6 +127,8 @@
</a-button> </a-button>
</actionDropdown> </actionDropdown>
<importApiDrawer v-model:visible="importApiDrawerVisible" /> <importApiDrawer v-model:visible="importApiDrawerVisible" />
<customApiDrawer v-model:visible="customApiDrawerVisible" />
<scriptOperationDrawer v-model:visible="scriptOperationDrawerVisible" />
</div> </div>
</template> </template>
@ -65,36 +137,58 @@
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';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import customApiDrawer from '../common/customApiDrawer.vue';
import executeStatus from '../common/executeStatus.vue'; import executeStatus from '../common/executeStatus.vue';
import importApiDrawer from '../common/importApiDrawer/index.vue'; import importApiDrawer from '../common/importApiDrawer/index.vue';
import scriptOperationDrawer from '../common/scriptOperationDrawer.vue';
import stepType from '../common/stepType.vue'; import stepType from '../common/stepType.vue';
import actionDropdown from './actionDropdown.vue'; import actionDropdown from './actionDropdown.vue';
import conditionContent from './stepNodeComposition/conditionContent.vue';
import customApiContent from './stepNodeComposition/customApiContent.vue';
import loopControlContent from './stepNodeComposition/loopContent.vue';
import onlyOnceControlContent from './stepNodeComposition/onlyOnceContent.vue';
import quoteContent from './stepNodeComposition/quoteContent.vue';
import waitTimeContent from './stepNodeComposition/waitTimeContent.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 { ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum'; import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { defaultStepItemCommon } from '../config';
export interface ScenarioStepItem { export interface ScenarioStepItem {
id: string | number; id: string | number;
order: number; order: number;
checked: boolean; enabled: boolean; //
type: ScenarioStepType; type: ScenarioStepType;
name: string; name: string;
description: string; description: string;
status: ScenarioExecuteStatus; method?: RequestMethods;
status?: ScenarioExecuteStatus;
projectId?: string;
children?: ScenarioStepItem[]; children?: ScenarioStepItem[];
//
checked: boolean; //
expanded: boolean; //
} }
const props = defineProps<{ const props = defineProps<{
steps: ScenarioStepItem[]; stepKeyword: string;
expandAll?: boolean;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const steps = defineModel<ScenarioStepItem[]>('steps', {
required: true,
});
const checkedKeys = defineModel<string[]>('checkedKeys', { const checkedKeys = defineModel<string[]>('checkedKeys', {
required: true, required: true,
}); });
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[] = [ const stepMoreActions: ActionsItem[] = [
@ -117,21 +211,78 @@
}, },
]; ];
function getStepContent(step: ScenarioStepItem) {
switch (step.type) {
case ScenarioStepType.QUOTE_API:
case ScenarioStepType.QUOTE_CASE:
case ScenarioStepType.QUOTE_SCENARIO:
return quoteContent;
case ScenarioStepType.CUSTOM_API:
return customApiContent;
case ScenarioStepType.LOOP_CONTROL:
return loopControlContent;
case ScenarioStepType.CONDITION_CONTROL:
return conditionContent;
case ScenarioStepType.ONLY_ONCE_CONTROL:
return onlyOnceControlContent;
case ScenarioStepType.WAIT_TIME:
return waitTimeContent;
default:
return '';
}
}
function setFocusNodeKey(node: MsTreeNodeData) { function setFocusNodeKey(node: MsTreeNodeData) {
focusStepKey.value = node.id || ''; focusStepKey.value = node.id || '';
} }
function toggleNodeExpand(node: MsTreeNodeData) { function checkStepIsApi(step: ScenarioStepItem) {
if (node.id) { return [ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API, ScenarioStepType.CUSTOM_API].includes(step.type);
treeRef.value?.expandNode(node.id, !node.expanded);
}
} }
function checkAll(val: boolean) { function checkAll(val: boolean) {
treeRef.value?.checkAll(val); treeRef.value?.checkAll(val);
} }
/**
* 处理步骤名称编辑
*/
const showStepNameEditInputStepId = ref<string | number>('');
const tempStepName = ref('');
function handleStepNameClick(step: ScenarioStepItem) {
tempStepName.value = step.name;
showStepNameEditInputStepId.value = step.id;
nextTick(() => {
const input = treeRef.value?.$el.querySelector('.arco-input') as HTMLInputElement;
input?.focus();
});
}
function applyStepChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) {
realStep.name = tempStepName.value;
}
showStepNameEditInputStepId.value = '';
}
/**
* 处理步骤展开折叠
*/
function handleStepExpand(data: MsTreeExpandedData) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.id, 'id');
if (realStep) {
realStep.expanded = !realStep.expanded;
}
}
function executeStep(node: MsTreeNodeData) {
console.log('执行步骤', node);
}
const importApiDrawerVisible = ref(false); const importApiDrawerVisible = ref(false);
const customApiDrawerVisible = ref(false);
const scriptOperationDrawerVisible = ref(false);
function handleActionSelect(val: ScenarioAddStepActionType) { function handleActionSelect(val: ScenarioAddStepActionType) {
switch (val) { switch (val) {
@ -139,22 +290,50 @@
importApiDrawerVisible.value = true; importApiDrawerVisible.value = true;
break; break;
case ScenarioAddStepActionType.CUSTOM_API: case ScenarioAddStepActionType.CUSTOM_API:
console.log('自定义API'); customApiDrawerVisible.value = true;
break; break;
case ScenarioAddStepActionType.LOOP_CONTROL: case ScenarioAddStepActionType.LOOP_CONTROL:
console.log('循环控制'); steps.value.push({
...defaultStepItemCommon,
id: Date.now(),
order: steps.value.length + 1,
type: ScenarioStepType.LOOP_CONTROL,
name: '循环控制',
description: '循环控制描述',
});
break; break;
case ScenarioAddStepActionType.CONDITION_CONTROL: case ScenarioAddStepActionType.CONDITION_CONTROL:
console.log('条件控制'); steps.value.push({
...defaultStepItemCommon,
id: Date.now(),
order: steps.value.length + 1,
type: ScenarioStepType.CONDITION_CONTROL,
name: '条件控制',
description: '条件控制描述',
});
break; break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL: case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
console.log('仅执行一次'); steps.value.push({
...defaultStepItemCommon,
id: Date.now(),
order: steps.value.length + 1,
type: ScenarioStepType.ONLY_ONCE_CONTROL,
name: '仅执行一次',
description: '仅执行一次描述',
});
break; break;
case ScenarioAddStepActionType.SCRIPT_OPERATION: case ScenarioAddStepActionType.SCRIPT_OPERATION:
console.log('脚本操作'); scriptOperationDrawerVisible.value = true;
break; break;
case ScenarioAddStepActionType.WAIT_TIME: case ScenarioAddStepActionType.WAIT_TIME:
console.log('等待时间'); steps.value.push({
...defaultStepItemCommon,
id: Date.now(),
order: steps.value.length + 1,
type: ScenarioStepType.WAIT_TIME,
name: '等待时间',
description: '等待时间描述',
});
break; break;
default: default:
break; break;
@ -180,7 +359,7 @@
background-color: rgb(var(--primary-1)); background-color: rgb(var(--primary-1));
} }
} }
// // TODO:transform
.loop-levels(@index, @max) when (@index <= @max) { .loop-levels(@index, @max) when (@index <= @max) {
:deep(.arco-tree-node[data-level='@{index}']) { :deep(.arco-tree-node[data-level='@{index}']) {
margin-left: @index * 32px; margin-left: @index * 32px;
@ -189,7 +368,7 @@
} }
.loop-levels(0, 99); // .loop-levels(0, 99); //
:deep(.arco-tree-node) { :deep(.arco-tree-node) {
padding: 7px 8px; padding: 0 8px;
border: 1px solid var(--color-text-n8); border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-medium) !important; border-radius: var(--border-radius-medium) !important;
&:not(:first-child) { &:not(:first-child) {
@ -202,19 +381,41 @@
} }
} }
.arco-tree-node-title { .arco-tree-node-title {
@apply !cursor-pointer;
padding: 12px 4px;
&:hover { &:hover {
background-color: var(--color-text-n9) !important; background-color: var(--color-text-n9) !important;
} }
.step-node-first { .step-node-content {
@apply flex items-center; @apply flex w-full flex-1 items-center;
gap: 8px; gap: 8px;
margin-right: 6px;
}
.step-name-container {
@apply flex items-center;
margin-right: 16px;
&:hover {
.edit-script-name-icon {
@apply visible;
}
}
.edit-script-name-icon {
@apply invisible cursor-pointer;
color: rgb(var(--primary-5));
}
} }
&[draggable='true']:hover { &[draggable='true']:hover {
.step-node-first { .step-node-content {
padding-left: 20px; padding-left: 20px;
} }
} }
.arco-tree-node-title-text {
@apply flex-1;
}
} }
.arco-tree-node-indent { .arco-tree-node-indent {
@apply hidden; @apply hidden;
@ -225,7 +426,7 @@
.arco-tree-node-drag-icon { .arco-tree-node-drag-icon {
@apply ml-0; @apply ml-0;
top: 6px; top: 13px;
left: 24px; left: 24px;
width: 16px; width: 16px;
height: 16px; height: 16px;

View File

@ -154,6 +154,9 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.arco-tabs-nav) {
@apply border-b;
}
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
@apply pt-0; @apply pt-0;
} }

View File

@ -99,6 +99,8 @@ export default {
'apiScenario.scenario': '场景', 'apiScenario.scenario': '场景',
'apiScenario.sumSelected': '共选择', 'apiScenario.sumSelected': '共选择',
'apiScenario.scenarioConfig': '场景配置', 'apiScenario.scenarioConfig': '场景配置',
'apiScenario.noMatchStep': '暂无匹配的步骤数据',
'apiScenario.pleaseInputStepName': '请输入步骤名称',
// 执行历史 // 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索', 'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号', 'apiScenario.executeHistory.num': '序号',