feat(接口场景): 场景步骤 10%
This commit is contained in:
parent
b99d7a60fa
commit
f5dc90ffd8
|
@ -443,16 +443,16 @@
|
|||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
margin-right: 24px;
|
||||
}
|
||||
.arco-form-item{
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.arco-icon-hover.arco-radio-icon-hover::before {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.arco-radio-checked:not(.arco-radio-disabled) {
|
||||
.arco-radio-icon {
|
||||
@apply !bg-white;
|
||||
|
@ -653,6 +653,12 @@
|
|||
.arco-switch {
|
||||
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-handle {
|
||||
width: 14px;
|
||||
|
@ -661,7 +667,7 @@
|
|||
}
|
||||
.arco-switch-type-line.arco-switch-small.arco-switch-checked {
|
||||
.arco-switch-handle {
|
||||
left: calc(100% - 14px - 0px);
|
||||
left: calc(100% - 14px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div ref="treeContainerRef" :class="['ms-tree-container', containerStatusClass]">
|
||||
<a-tree
|
||||
v-show="treeData.length > 0"
|
||||
v-show="data.length > 0"
|
||||
v-bind="props"
|
||||
ref="treeRef"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:data="treeData"
|
||||
:data="data"
|
||||
class="ms-tree"
|
||||
:allow-drop="handleAllowDrop"
|
||||
@drag-start="onDragStart"
|
||||
|
@ -15,6 +15,7 @@
|
|||
@drop="onDrop"
|
||||
@select="select"
|
||||
@check="checked"
|
||||
@expand="expand"
|
||||
>
|
||||
<template v-if="$slots['title']" #title="_props">
|
||||
<a-tooltip
|
||||
|
@ -64,7 +65,7 @@
|
|||
</a-tree>
|
||||
<slot name="empty">
|
||||
<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)]"
|
||||
>
|
||||
{{ props.emptyText }}
|
||||
|
@ -74,8 +75,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { nextTick, onBeforeMount, Ref, ref, watch } from 'vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/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 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';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: MsTreeNodeData[];
|
||||
keyword?: string; // 搜索关键字
|
||||
searchDebounce?: number; // 搜索防抖 ms 数
|
||||
draggable?: boolean; // 是否可拖拽
|
||||
|
@ -107,6 +106,8 @@
|
|||
checkedStrategy?: 'all' | 'parent' | 'child'; // 选中节点时的策略
|
||||
virtualListProps?: VirtualListProps; // 虚拟滚动列表的属性
|
||||
disabledTitleTooltip?: boolean; // 是否禁用标题 tooltip
|
||||
actionOnNodeClick?: 'expand'; // 点击节点时的操作
|
||||
nodeHighlightBackgroundColor?: string; // 节点高亮背景色
|
||||
titleTooltipPosition?:
|
||||
| 'top'
|
||||
| 'tl'
|
||||
|
@ -151,8 +152,12 @@
|
|||
(e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void;
|
||||
(e: 'moreActionsClose'): 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', {
|
||||
default: [],
|
||||
});
|
||||
|
@ -172,25 +177,8 @@
|
|||
overHeight: 32,
|
||||
containerClassName: 'ms-tree-container',
|
||||
});
|
||||
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;
|
||||
// });
|
||||
nextTick(() => {
|
||||
if (isFirstInit) {
|
||||
if (props.defaultExpandAll) {
|
||||
|
@ -212,10 +200,18 @@
|
|||
init(true);
|
||||
});
|
||||
|
||||
const originTreeData = ref<MsTreeNodeData[]>([]); // 初始化时全量的树数据或在非搜索情况下更新后的全量树数据
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
init();
|
||||
() => data.value,
|
||||
(val) => {
|
||||
if (!props.keyword) {
|
||||
originTreeData.value = cloneDeep(val);
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -224,16 +220,17 @@
|
|||
* @param keyword 搜索关键字
|
||||
*/
|
||||
function searchData(keyword: string) {
|
||||
const search = (data: MsTreeNodeData[]) => {
|
||||
const search = (_data: MsTreeNodeData[]) => {
|
||||
const result: MsTreeNodeData[] = [];
|
||||
data.forEach((item) => {
|
||||
_data.forEach((item) => {
|
||||
if (item[props.fieldNames.title].toLowerCase().indexOf(keyword.toLowerCase()) > -1) {
|
||||
result.push({ ...item });
|
||||
result.push({ ...item, expanded: true });
|
||||
} else if (item[props.fieldNames.children]) {
|
||||
const filterData = search(item[props.fieldNames.children]);
|
||||
if (filterData.length) {
|
||||
result.push({
|
||||
...item,
|
||||
expanded: true,
|
||||
[props.fieldNames.children]: filterData,
|
||||
});
|
||||
}
|
||||
|
@ -243,25 +240,26 @@
|
|||
return result;
|
||||
};
|
||||
|
||||
return search(originalTreeData.value);
|
||||
return search(originTreeData.value);
|
||||
}
|
||||
|
||||
const treeData = ref<MsTreeNodeData[]>([]);
|
||||
|
||||
// 防抖搜索
|
||||
const updateDebouncedSearch = debounce(() => {
|
||||
if (props.keyword) {
|
||||
treeData.value = searchData(props.keyword);
|
||||
data.value = searchData(props.keyword);
|
||||
}
|
||||
}, props.searchDebounce);
|
||||
|
||||
watchEffect(() => {
|
||||
if (!props.keyword) {
|
||||
treeData.value = originalTreeData.value;
|
||||
} else {
|
||||
updateDebouncedSearch();
|
||||
watch(
|
||||
() => props.keyword,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
data.value = cloneDeep(originTreeData.value);
|
||||
} else {
|
||||
updateDebouncedSearch();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
function loop(
|
||||
_data: MsTreeNodeData[],
|
||||
|
@ -309,13 +307,13 @@
|
|||
dropNode: MsTreeNodeData; // 放入的节点
|
||||
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);
|
||||
});
|
||||
|
||||
if (dropPosition === 0) {
|
||||
// 放入节点内
|
||||
loop(originalTreeData.value, dropNode.key, (item) => {
|
||||
loop(data.value, dropNode.key, (item) => {
|
||||
item.children = item.children || [];
|
||||
item.children.push(dragNode);
|
||||
});
|
||||
|
@ -325,18 +323,18 @@
|
|||
}
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
emit('drop', originalTreeData.value, dragNode, dropNode, dropPosition);
|
||||
emit('drop', data.value, dragNode, dropNode, dropPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理树节点选中(非复选框)
|
||||
*/
|
||||
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>) {
|
||||
|
@ -351,7 +349,7 @@
|
|||
if (val?.toString() !== '') {
|
||||
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
|
||||
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) {
|
||||
focusEl.value.style.backgroundColor = '';
|
||||
|
@ -380,6 +378,10 @@
|
|||
}
|
||||
);
|
||||
|
||||
function expand(expandKeys: Array<string | number>, node: MsTreeExpandedData) {
|
||||
emit('expand', node);
|
||||
}
|
||||
|
||||
function checkAll(val: boolean) {
|
||||
treeRef.value?.checkAll(val);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface MsTreeFieldNames extends TreeFieldNames {
|
|||
export type MsTreeNodeData = {
|
||||
hideMoreAction?: boolean; // 隐藏更多操作
|
||||
parentId?: string;
|
||||
expanded?: boolean; // 是否展开
|
||||
[key: string]: any;
|
||||
} & TreeNodeData;
|
||||
|
||||
|
@ -28,3 +29,10 @@ export interface MsTreeSelectedData {
|
|||
node?: MsTreeNodeData;
|
||||
e?: Event;
|
||||
}
|
||||
|
||||
export interface MsTreeExpandedData {
|
||||
expanded?: boolean;
|
||||
expandedNodes: MsTreeNodeData[];
|
||||
node?: MsTreeNodeData;
|
||||
e?: Event;
|
||||
}
|
||||
|
|
|
@ -268,7 +268,11 @@ export function filterTree<T>(
|
|||
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
||||
* @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++) {
|
||||
const node = trees[i];
|
||||
if (node[customKey] === targetKey) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -2,11 +2,11 @@
|
|||
<div
|
||||
class="rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
|
||||
:style="{
|
||||
borderColor: status.color,
|
||||
color: status.color,
|
||||
borderColor: type.color,
|
||||
color: type.color,
|
||||
}"
|
||||
>
|
||||
{{ status.label }}
|
||||
{{ type.label }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
|||
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
status: ScenarioStepType;
|
||||
type: ScenarioStepType;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -37,8 +37,8 @@
|
|||
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
|
||||
};
|
||||
|
||||
const status = computed(() => {
|
||||
const config = scenarioStepMap[props.status];
|
||||
const type = computed(() => {
|
||||
const config = scenarioStepMap[props.type];
|
||||
return {
|
||||
border: `1px solid ${config?.color}`,
|
||||
color: config?.color,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const defaultStepItemCommon = {
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export default {};
|
|
@ -61,13 +61,11 @@
|
|||
</div>
|
||||
</template>
|
||||
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
|
||||
<a-input-search
|
||||
<a-input
|
||||
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"
|
||||
|
@ -82,7 +80,13 @@
|
|||
</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>
|
||||
</template>
|
||||
|
@ -96,7 +100,7 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
export interface ScenarioStepInfo {
|
||||
id: string | number;
|
||||
|
@ -125,16 +129,20 @@
|
|||
id: 1,
|
||||
order: 1,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_API,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
method: RequestMethods.GET,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
order: 1,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_CASE,
|
||||
name: 'API11',
|
||||
description: 'API11描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
|
@ -143,7 +151,9 @@
|
|||
id: 12,
|
||||
order: 2,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_SCENARIO,
|
||||
name: 'API12',
|
||||
description: 'API12描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
|
@ -154,7 +164,20 @@
|
|||
id: 2,
|
||||
order: 2,
|
||||
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',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
|
@ -215,10 +238,6 @@
|
|||
function refreshStepInfo() {
|
||||
console.log('刷新步骤信息');
|
||||
}
|
||||
|
||||
function searchStep(val: string) {
|
||||
stepInfo.value.steps = stepInfo.value.steps.filter((item) => item.name.includes(val));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>condition </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>custom </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>loop </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>only once </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>quote </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div>waitTime </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -1,50 +1,120 @@
|
|||
<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 class="max-h-[calc(100vh-305px)]">
|
||||
<MsTree
|
||||
ref="treeRef"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:focus-node-key="focusStepKey"
|
||||
v-model:data="steps"
|
||||
:keyword="props.stepKeyword"
|
||||
: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,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
}"
|
||||
node-highlight-background-color="var(--color-text-n9)"
|
||||
action-on-node-click="expand"
|
||||
disabled-title-tooltip
|
||||
checkable
|
||||
block-node
|
||||
draggable
|
||||
@expand="handleStepExpand"
|
||||
>
|
||||
<template #title="step">
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
<!-- 步骤序号 -->
|
||||
<div
|
||||
v-show="step.children?.length > 0"
|
||||
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"
|
||||
@click.stop="toggleNodeExpand(step)"
|
||||
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
|
||||
>
|
||||
<MsIcon
|
||||
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
|
||||
:size="14"
|
||||
/>
|
||||
{{ step.children?.length || 0 }}
|
||||
{{ step.order }}
|
||||
</div>
|
||||
<div class="step-node-content">
|
||||
<!-- 步骤展开折叠按钮 -->
|
||||
<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 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>
|
||||
</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>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<executeStatus v-if="step.status" :status="step.status" size="small" />
|
||||
</template>
|
||||
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
|
||||
<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
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
|
||||
|
@ -57,6 +127,8 @@
|
|||
</a-button>
|
||||
</actionDropdown>
|
||||
<importApiDrawer v-model:visible="importApiDrawerVisible" />
|
||||
<customApiDrawer v-model:visible="customApiDrawerVisible" />
|
||||
<scriptOperationDrawer v-model:visible="scriptOperationDrawerVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -65,36 +137,58 @@
|
|||
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 { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
import customApiDrawer from '../common/customApiDrawer.vue';
|
||||
import executeStatus from '../common/executeStatus.vue';
|
||||
import importApiDrawer from '../common/importApiDrawer/index.vue';
|
||||
import scriptOperationDrawer from '../common/scriptOperationDrawer.vue';
|
||||
import stepType from '../common/stepType.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 { 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 {
|
||||
id: string | number;
|
||||
order: number;
|
||||
checked: boolean;
|
||||
enabled: boolean; // 是否启用
|
||||
type: ScenarioStepType;
|
||||
name: string;
|
||||
description: string;
|
||||
status: ScenarioExecuteStatus;
|
||||
method?: RequestMethods;
|
||||
status?: ScenarioExecuteStatus;
|
||||
projectId?: string;
|
||||
children?: ScenarioStepItem[];
|
||||
// 页面渲染以及交互需要字段
|
||||
checked: boolean; // 是否选中
|
||||
expanded: boolean; // 是否展开
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
steps: ScenarioStepItem[];
|
||||
stepKeyword: string;
|
||||
expandAll?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const steps = defineModel<ScenarioStepItem[]>('steps', {
|
||||
required: true,
|
||||
});
|
||||
const checkedKeys = defineModel<string[]>('checkedKeys', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string>(''); // 聚焦的key
|
||||
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) {
|
||||
focusStepKey.value = node.id || '';
|
||||
}
|
||||
|
||||
function toggleNodeExpand(node: MsTreeNodeData) {
|
||||
if (node.id) {
|
||||
treeRef.value?.expandNode(node.id, !node.expanded);
|
||||
}
|
||||
function checkStepIsApi(step: ScenarioStepItem) {
|
||||
return [ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API, ScenarioStepType.CUSTOM_API].includes(step.type);
|
||||
}
|
||||
|
||||
function checkAll(val: boolean) {
|
||||
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 customApiDrawerVisible = ref(false);
|
||||
const scriptOperationDrawerVisible = ref(false);
|
||||
|
||||
function handleActionSelect(val: ScenarioAddStepActionType) {
|
||||
switch (val) {
|
||||
|
@ -139,22 +290,50 @@
|
|||
importApiDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
console.log('自定义API');
|
||||
customApiDrawerVisible.value = true;
|
||||
break;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
console.log('脚本操作');
|
||||
scriptOperationDrawerVisible.value = true;
|
||||
break;
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
|
@ -180,7 +359,7 @@
|
|||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
// 循环生成树的左边距样式
|
||||
// 循环生成树的左边距样式 TODO:transform性能更高以及保留步骤完整宽度,需要加横向滚动
|
||||
.loop-levels(@index, @max) when (@index <= @max) {
|
||||
:deep(.arco-tree-node[data-level='@{index}']) {
|
||||
margin-left: @index * 32px;
|
||||
|
@ -189,7 +368,7 @@
|
|||
}
|
||||
.loop-levels(0, 99); // 最大层级
|
||||
:deep(.arco-tree-node) {
|
||||
padding: 7px 8px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-medium) !important;
|
||||
&:not(:first-child) {
|
||||
|
@ -202,19 +381,41 @@
|
|||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
@apply !cursor-pointer;
|
||||
|
||||
padding: 12px 4px;
|
||||
&:hover {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
.step-node-first {
|
||||
@apply flex items-center;
|
||||
.step-node-content {
|
||||
@apply flex w-full flex-1 items-center;
|
||||
|
||||
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 {
|
||||
.step-node-first {
|
||||
.step-node-content {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title-text {
|
||||
@apply flex-1;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-indent {
|
||||
@apply hidden;
|
||||
|
@ -225,7 +426,7 @@
|
|||
.arco-tree-node-drag-icon {
|
||||
@apply ml-0;
|
||||
|
||||
top: 6px;
|
||||
top: 13px;
|
||||
left: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
|
|
@ -154,6 +154,9 @@
|
|||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-tabs-nav) {
|
||||
@apply border-b;
|
||||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
@apply pt-0;
|
||||
}
|
||||
|
|
|
@ -99,6 +99,8 @@ export default {
|
|||
'apiScenario.scenario': '场景',
|
||||
'apiScenario.sumSelected': '共选择',
|
||||
'apiScenario.scenarioConfig': '场景配置',
|
||||
'apiScenario.noMatchStep': '暂无匹配的步骤数据',
|
||||
'apiScenario.pleaseInputStepName': '请输入步骤名称',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
Loading…
Reference in New Issue