feat(接口场景): 场景步骤 5%
This commit is contained in:
parent
e76308113e
commit
f624884fd7
|
@ -37,7 +37,7 @@ import {
|
|||
ScenarioHistoryItem,
|
||||
ScenarioHistoryPageParams,
|
||||
} from '@/models/apiTest/scenario';
|
||||
import { AddModuleParams, BatchApiParams, CommonList, ModuleTreeNode, MoveModules } from '@/models/common';
|
||||
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules } from '@/models/common';
|
||||
|
||||
// 更新模块
|
||||
export function updateModule(data: ApiScenarioModuleUpdateParams) {
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
v-bind="props"
|
||||
ref="treeRef"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selected-keys="innerSelectedKeys"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:data="treeData"
|
||||
class="ms-tree"
|
||||
:allow-drop="handleAllowDrop"
|
||||
|
@ -20,6 +21,7 @@
|
|||
:content="_props[props.fieldNames.title]"
|
||||
:mouse-enter-delay="800"
|
||||
:position="props.titleTooltipPosition"
|
||||
:disabled="props.disabledTitleTooltip"
|
||||
>
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
</a-tooltip>
|
||||
|
@ -32,34 +34,31 @@
|
|||
v-if="_props.hideMoreAction !== true"
|
||||
:class="[
|
||||
'ms-tree-node-extra',
|
||||
innerFocusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '', // TODO:通过下拉菜单的显示隐藏去控制聚焦状态似乎能有更好的性能
|
||||
focusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '', // TODO:通过下拉菜单的显示隐藏去控制聚焦状态似乎能有更好的性能
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="ml-[-4px] flex h-[32px] items-center rounded-[var(--border-radius-small)] bg-[rgb(var(--primary-1))]"
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
:list="
|
||||
typeof props.filterMoreActionFunc === 'function'
|
||||
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
|
||||
: props.nodeMoreActions
|
||||
"
|
||||
trigger="click"
|
||||
@select="handleNodeMoreSelect($event, _props)"
|
||||
@close="moreActionsClose"
|
||||
>
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
:list="
|
||||
typeof props.filterMoreActionFunc === 'function'
|
||||
? props.filterMoreActionFunc(props.nodeMoreActions, _props)
|
||||
: props.nodeMoreActions
|
||||
"
|
||||
trigger="click"
|
||||
@select="handleNodeMoreSelect($event, _props)"
|
||||
@close="moreActionsClose"
|
||||
<MsButton
|
||||
type="text"
|
||||
:size="props.nodeMoreActionSize || 'mini'"
|
||||
class="ms-tree-node-extra__more"
|
||||
@click="focusNodeKey = _props[props.fieldNames.key]"
|
||||
>
|
||||
<MsButton
|
||||
type="text"
|
||||
size="mini"
|
||||
class="ms-tree-node-extra__more"
|
||||
@click="innerFocusNodeKey = _props[props.fieldNames.key]"
|
||||
>
|
||||
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</MsTableMoreAction>
|
||||
</div>
|
||||
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</MsTableMoreAction>
|
||||
<slot name="extraEnd" v-bind="_props"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
|
@ -75,8 +74,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
@ -101,15 +99,14 @@
|
|||
defaultExpandAll?: boolean; // 是否默认展开所有节点
|
||||
selectable?: boolean | ((node: MsTreeNodeData, info: { level: number; isLeaf: boolean }) => boolean); // 是否可选中
|
||||
fieldNames?: MsTreeFieldNames; // 自定义字段名
|
||||
focusNodeKey?: string | number; // 聚焦的节点 key
|
||||
selectedKeys?: Array<string | number>; // 选中的节点 key
|
||||
nodeMoreActions?: ActionsItem[]; // 节点展示在省略号按钮内的更多操作
|
||||
nodeMoreActionSize?: 'medium' | 'mini' | 'small' | 'large'; // 更多操作按钮大小
|
||||
expandAll?: boolean; // 是否展开/折叠所有节点,true 为全部展开,false 为全部折叠
|
||||
emptyText?: string; // 空数据时的文案
|
||||
checkable?: boolean; // 是否可选中
|
||||
checkedStrategy?: 'all' | 'parent' | 'child'; // 选中节点时的策略
|
||||
checkedKeys?: Array<string | number>; // 选中的节点 key
|
||||
virtualListProps?: VirtualListProps; // 虚拟滚动列表的属性
|
||||
disabledTitleTooltip?: boolean; // 是否禁用标题 tooltip
|
||||
titleTooltipPosition?:
|
||||
| 'top'
|
||||
| 'tl'
|
||||
|
@ -138,6 +135,7 @@
|
|||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}),
|
||||
disabledTitleTooltip: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -151,12 +149,23 @@
|
|||
dropPosition: number // 放入的位置,-1 为放入节点前,1 为放入节点后,0 为放入节点内
|
||||
): void;
|
||||
(e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void;
|
||||
(e: 'update:focusNodeKey', val: string | number): void;
|
||||
(e: 'update:selectedKeys', val: Array<string | number>): void;
|
||||
(e: 'moreActionsClose'): void;
|
||||
(e: 'check', val: Array<string | number>): void;
|
||||
}>();
|
||||
|
||||
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
|
||||
default: [],
|
||||
});
|
||||
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
|
||||
default: [],
|
||||
});
|
||||
const expandedKeys = defineModel<(string | number)[]>('expandedKeys', {
|
||||
default: [],
|
||||
});
|
||||
const focusNodeKey = defineModel<string | number>('focusNodeKey', {
|
||||
default: '',
|
||||
});
|
||||
|
||||
const treeContainerRef: Ref = ref(null);
|
||||
const treeRef: Ref = ref(null);
|
||||
const { isInitListener, containerStatusClass, setContainer, initScrollListener } = useContainerShadow({
|
||||
|
@ -166,21 +175,22 @@
|
|||
const originalTreeData = ref<MsTreeNodeData[]>([]);
|
||||
|
||||
function init(isFirstInit = false) {
|
||||
originalTreeData.value = mapTree<MsTreeNodeData>(props.data, (node: MsTreeNodeData) => {
|
||||
if (!props.showLine) {
|
||||
// 不展示连接线时才设置节点图标,因为展示连接线时非叶子节点会展示默认的折叠图标。它不会覆盖 switcherIcon,但是会被 switcherIcon 覆盖
|
||||
node.icon = () => h('span', { class: 'hidden' });
|
||||
}
|
||||
if (
|
||||
node[props.fieldNames.isLeaf || 'isLeaf'] ||
|
||||
!node[props.fieldNames.children] ||
|
||||
node[props.fieldNames.children]?.length === 0
|
||||
) {
|
||||
// 设置子节点图标,会覆盖 icon。当展示连接线时,需要设置 switcherIcon 以覆盖组件的默认图标;不展示连接线则是 icon
|
||||
node[props.showLine ? 'switcherIcon' : 'icon'] = () => h('span', { class: 'hidden' });
|
||||
}
|
||||
return node;
|
||||
});
|
||||
originalTreeData.value = mapTree<MsTreeNodeData>(props.data);
|
||||
// (node: MsTreeNodeData) => {
|
||||
// // if (!props.showLine) {
|
||||
// // // 不展示连接线时才设置节点图标,因为展示连接线时非叶子节点会展示默认的折叠图标。它不会覆盖 switcherIcon,但是会被 switcherIcon 覆盖
|
||||
// // node.icon = () => h('span', { class: 'hidden' });
|
||||
// // }
|
||||
// // if (
|
||||
// // node[props.fieldNames.isLeaf || 'isLeaf'] ||
|
||||
// // !node[props.fieldNames.children] ||
|
||||
// // node[props.fieldNames.children]?.length === 0
|
||||
// // ) {
|
||||
// // // 设置子节点图标,会覆盖 icon。当展示连接线时,需要设置 switcherIcon 以覆盖组件的默认图标;不展示连接线则是 icon
|
||||
// // node[props.showLine ? 'switcherIcon' : 'icon'] = () => h('span', { class: 'hidden' });
|
||||
// // }
|
||||
// return node;
|
||||
// });
|
||||
nextTick(() => {
|
||||
if (isFirstInit) {
|
||||
if (props.defaultExpandAll) {
|
||||
|
@ -209,8 +219,6 @@
|
|||
}
|
||||
);
|
||||
|
||||
const expandedKeys = ref<(string | number)[]>([]);
|
||||
|
||||
/**
|
||||
* 根据关键字过滤树节点
|
||||
* @param keyword 搜索关键字
|
||||
|
@ -327,20 +335,18 @@
|
|||
/**
|
||||
* 处理树节点选中(非复选框)
|
||||
*/
|
||||
function select(selectedKeys: Array<string | number>, data: MsTreeSelectedData) {
|
||||
emit('select', selectedKeys, data.selectedNodes[0]);
|
||||
function select(_selectedKeys: Array<string | number>, data: MsTreeSelectedData) {
|
||||
emit('select', _selectedKeys, data.selectedNodes[0]);
|
||||
}
|
||||
|
||||
function checked(checkedKeys: Array<string | number>) {
|
||||
emit('check', checkedKeys);
|
||||
function checked(_checkedKeys: Array<string | number>) {
|
||||
emit('check', _checkedKeys);
|
||||
}
|
||||
|
||||
const innerFocusNodeKey = useVModel(props, 'focusNodeKey', emit); // 聚焦的节点,一般用于在操作扩展按钮时,高亮当前节点,保持扩展按钮持续显示
|
||||
|
||||
const focusEl = ref<HTMLElement | null>(); // 存储聚焦的节点元素
|
||||
|
||||
watch(
|
||||
() => innerFocusNodeKey.value,
|
||||
() => focusNodeKey.value,
|
||||
(val) => {
|
||||
if (val?.toString() !== '') {
|
||||
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
|
||||
|
@ -374,7 +380,18 @@
|
|||
}
|
||||
);
|
||||
|
||||
const innerSelectedKeys = useVModel(props, 'selectedKeys', emit);
|
||||
function checkAll(val: boolean) {
|
||||
treeRef.value?.checkAll(val);
|
||||
}
|
||||
|
||||
function expandNode(key: string | number, expanded: boolean) {
|
||||
treeRef.value?.expandNode(key, expanded);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
checkAll,
|
||||
expandNode,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -443,7 +460,12 @@
|
|||
width: 60%;
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply invisible relative w-0;
|
||||
@apply invisible relative flex w-0 items-center;
|
||||
|
||||
margin-left: -4px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: rgb(var(--primary-1));
|
||||
&:hover {
|
||||
@apply visible w-auto;
|
||||
}
|
||||
|
|
|
@ -138,4 +138,5 @@ export default {
|
|||
'common.batchDelete': 'Batch delete',
|
||||
'common.batchDebug': 'Batch debug',
|
||||
'common.quote': 'Quote',
|
||||
'common.execute': '执行',
|
||||
};
|
||||
|
|
|
@ -141,4 +141,5 @@ export default {
|
|||
'common.batchDelete': '批量删除',
|
||||
'common.batchDebug': '批量调试',
|
||||
'common.quote': '引用',
|
||||
'common.execute': '执行',
|
||||
};
|
||||
|
|
|
@ -199,7 +199,8 @@ export function mapTree<T>(
|
|||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
customNodeFn: (node: TreeNode<T>, path: string) => TreeNode<T> | null = (node) => node,
|
||||
customChildrenKey = 'children',
|
||||
parentPath = ''
|
||||
parentPath = '',
|
||||
level = 0
|
||||
): T[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
tree = [tree];
|
||||
|
@ -210,8 +211,17 @@ export function mapTree<T>(
|
|||
const fullPath = node.path ? `${parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
|
||||
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
|
||||
|
||||
if (newNode && newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
|
||||
newNode[customChildrenKey] = mapTree(newNode[customChildrenKey], customNodeFn, customChildrenKey);
|
||||
if (newNode) {
|
||||
newNode.level = level;
|
||||
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
|
||||
newNode[customChildrenKey] = mapTree(
|
||||
newNode[customChildrenKey],
|
||||
customNodeFn,
|
||||
customChildrenKey,
|
||||
fullPath,
|
||||
level + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return newNode;
|
||||
|
|
|
@ -25,7 +25,13 @@
|
|||
@change="resetModuleAndTable"
|
||||
/>
|
||||
</div>
|
||||
<moduleTree ref="moduleTreeRef" :type="activeKey" :protocol="protocol" @select="handleModuleSelect" />
|
||||
<moduleTree
|
||||
ref="moduleTreeRef"
|
||||
:type="activeKey"
|
||||
:project-id="currentProject"
|
||||
:protocol="protocol"
|
||||
@select="handleModuleSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
|
@ -143,7 +149,7 @@
|
|||
const moduleIds = ref<(string | number)[]>([]);
|
||||
|
||||
function resetModuleAndTable() {
|
||||
moduleTreeRef.value?.init();
|
||||
moduleTreeRef.value?.init(activeKey.value);
|
||||
apiTableRef.value?.loadPage(['root']); // 这里传入根模块id,因为模块需要加载,且默认选中的就是默认模块
|
||||
}
|
||||
|
||||
|
@ -180,6 +186,9 @@
|
|||
if (val) {
|
||||
resetModuleAndTable();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -54,8 +54,11 @@
|
|||
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
|
||||
import { getModuleCount, getModuleTreeOnlyModules } from '@/api/modules/api-test/management';
|
||||
import {
|
||||
getModuleCount as getScenarioModuleCount,
|
||||
getModuleTree as getScenarioModuleTree,
|
||||
} from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { mapTree } from '@/utils';
|
||||
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
|
@ -64,6 +67,7 @@
|
|||
defineProps<{
|
||||
type: 'api' | 'case' | 'scenario';
|
||||
protocol: string;
|
||||
projectId: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'api',
|
||||
|
@ -73,7 +77,6 @@
|
|||
(e: 'select', ids: (string | number)[], node: MsTreeNodeData): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const moduleKeyword = ref('');
|
||||
|
@ -86,16 +89,21 @@
|
|||
/**
|
||||
* 初始化模块树
|
||||
*/
|
||||
async function initModules() {
|
||||
async function initModules(type = props.type) {
|
||||
try {
|
||||
loading.value = true;
|
||||
folderTree.value = await getModuleTreeOnlyModules({
|
||||
// 只查看模块
|
||||
const params = {
|
||||
keyword: moduleKeyword.value,
|
||||
protocol: props.protocol,
|
||||
projectId: appStore.currentProjectId,
|
||||
projectId: props.projectId,
|
||||
moduleIds: [],
|
||||
});
|
||||
};
|
||||
if (type === 'api' || type === 'case') {
|
||||
// case 的模块与 api 的一致
|
||||
folderTree.value = await getModuleTreeOnlyModules(params);
|
||||
} else if (type === 'scenario') {
|
||||
folderTree.value = await getScenarioModuleTree(params);
|
||||
}
|
||||
selectedKeys.value = [folderTree.value[0]?.id];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -105,14 +113,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function initModuleCount() {
|
||||
async function initModuleCount(type = props.type) {
|
||||
try {
|
||||
moduleCountMap.value = await getModuleCount({
|
||||
const params = {
|
||||
keyword: moduleKeyword.value,
|
||||
protocol: props.protocol,
|
||||
projectId: appStore.currentProjectId,
|
||||
projectId: props.projectId,
|
||||
moduleIds: [],
|
||||
});
|
||||
};
|
||||
if (type === 'api' || type === 'case') {
|
||||
// case 的模块与 api 的一致
|
||||
moduleCountMap.value = await getModuleCount(params);
|
||||
} else if (type === 'scenario') {
|
||||
moduleCountMap.value = await getScenarioModuleCount(params);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -128,9 +142,9 @@
|
|||
emit('select', [keys[0], ...offspringIds], node);
|
||||
}
|
||||
|
||||
function init() {
|
||||
initModules();
|
||||
initModuleCount();
|
||||
function init(type = props.type) {
|
||||
initModules(type);
|
||||
initModuleCount(type);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
sorter: true,
|
||||
},
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'apiTestManagement.apiName',
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<a-dropdown class="scenario-action-dropdown" @select="(val) => emit('select', val as ScenarioAddStepActionType)">
|
||||
<slot></slot>
|
||||
<template #content>
|
||||
<a-dgroup :title="t('apiScenario.requestScenario')">
|
||||
<a-doption :value="ScenarioAddStepActionType.IMPORT_SYSTEM_API">
|
||||
{{ t('apiScenario.importSystemApi') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.CUSTOM_API">
|
||||
{{ t('apiScenario.customApi') }}
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup :title="t('apiScenario.logicControl')">
|
||||
<a-doption :value="ScenarioAddStepActionType.LOOP_CONTROL">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
{{ t('apiScenario.loopControl') }}
|
||||
<MsButton type="text" @click="openTutorial">{{ t('apiScenario.tutorial') }}</MsButton>
|
||||
</div>
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.CONDITION_CONTROL">
|
||||
{{ t('apiScenario.conditionControl') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.ONLY_ONCE_CONTROL">
|
||||
{{ t('apiScenario.onlyOnceControl') }}
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup :title="t('apiScenario.other')">
|
||||
<a-doption :value="ScenarioAddStepActionType.SCRIPT_OPERATION">
|
||||
{{ t('apiScenario.scriptOperation') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.WAIT_TIME">{{ t('apiScenario.waitTime') }}</a-doption>
|
||||
</a-dgroup>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioAddStepActionType } from '@/enums/apiEnum';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', val: ScenarioAddStepActionType): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function openTutorial() {
|
||||
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -3,8 +3,9 @@
|
|||
<div class="action-line">
|
||||
<div class="action-group">
|
||||
<a-checkbox
|
||||
v-model:model-value="stepInfo.checkedAll"
|
||||
:indeterminate="stepInfo.indeterminate"
|
||||
v-show="stepInfo.steps.length > 0"
|
||||
v-model:model-value="checkedAll"
|
||||
:indeterminate="indeterminate"
|
||||
@change="handleChangeAll"
|
||||
/>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
|
@ -13,32 +14,35 @@
|
|||
{{ t('apiScenario.steps') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="stepInfo.checkedAll || stepInfo.indeterminate" class="action-group">
|
||||
<a-tooltip :content="stepInfo.isExpand ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
|
||||
<div class="action-group">
|
||||
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
|
||||
<a-button
|
||||
v-show="stepInfo.steps.length > 0"
|
||||
type="outline"
|
||||
class="expand-step-btn arco-btn-outline--secondary"
|
||||
size="mini"
|
||||
@click="expandAllStep"
|
||||
>
|
||||
<MsIcon v-if="stepInfo.isExpand" type="icon-icon_comment_collapse_text_input" />
|
||||
<MsIcon v-if="isExpandAll" type="icon-icon_comment_collapse_text_input" />
|
||||
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-button type="outline" size="mini" @click="batchEnable">
|
||||
{{ t('common.batchEnable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDisable">
|
||||
{{ t('common.batchDisable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDebug">
|
||||
{{ t('common.batchDebug') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDelete">
|
||||
{{ t('common.batchDelete') }}
|
||||
</a-button>
|
||||
<template v-if="checkedAll || indeterminate">
|
||||
<a-button type="outline" size="mini" @click="batchEnable">
|
||||
{{ t('common.batchEnable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDisable">
|
||||
{{ t('common.batchDisable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDebug">
|
||||
{{ t('common.batchDebug') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDelete">
|
||||
{{ t('common.batchDelete') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="stepInfo.executeTime">
|
||||
<div class="action-group">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div>
|
||||
<div class="text-[var(--color-text-4)]">{{ stepInfo.executeTime }}</div>
|
||||
|
@ -55,126 +59,137 @@
|
|||
</div>
|
||||
<MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton>
|
||||
</div>
|
||||
<div class="action-group ml-auto">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiScenario.searchByName')"
|
||||
allow-clear
|
||||
class="w-[200px]"
|
||||
@search="searchStep"
|
||||
@press-enter="searchStep"
|
||||
/>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !mr-0 !p-[8px]" @click="refreshStepInfo">
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiScenario.searchByName')"
|
||||
allow-clear
|
||||
class="w-[200px]"
|
||||
@search="searchStep"
|
||||
@press-enter="searchStep"
|
||||
/>
|
||||
<a-button
|
||||
v-if="!props.isNew"
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary !mr-0 !p-[8px]"
|
||||
@click="refreshStepInfo"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<stepTree ref="stepTreeRef" v-model:checked-keys="checkedKeys" :steps="stepInfo.steps" />
|
||||
</div>
|
||||
<a-dropdown
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
|
||||
>
|
||||
<a-button type="dashed" class="add-step-btn" long>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<icon-plus />
|
||||
{{ t('apiScenario.addStep') }}
|
||||
</div>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-dgroup :title="t('apiScenario.requestScenario')">
|
||||
<a-doption :value="ScenarioAddStepActionType.IMPORT_SYSTEM_API">
|
||||
{{ t('apiScenario.importSystemApi') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.CUSTOM_API">
|
||||
{{ t('apiScenario.customApi') }}
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup :title="t('apiScenario.logicControl')">
|
||||
<a-doption :value="ScenarioAddStepActionType.LOOP_CONTROL">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
{{ t('apiScenario.loopControl') }}
|
||||
<MsButton type="text" @click="openTutorial">{{ t('apiScenario.tutorial') }}</MsButton>
|
||||
</div>
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.CONDITION_CONTROL">
|
||||
{{ t('apiScenario.conditionControl') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.ONLY_ONCE_CONTROL">
|
||||
{{ t('apiScenario.onlyOnceControl') }}
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup :title="t('apiScenario.other')">
|
||||
<a-doption :value="ScenarioAddStepActionType.SCRIPT_OPERATION">
|
||||
{{ t('apiScenario.scriptOperation') }}
|
||||
</a-doption>
|
||||
<a-doption :value="ScenarioAddStepActionType.WAIT_TIME">{{ t('apiScenario.waitTime') }}</a-doption>
|
||||
</a-dgroup>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<importApiDrawer v-model:visible="importApiDrawerVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import executeStatus from '../common/executeStatus.vue';
|
||||
import importApiDrawer from '../common/importApiDrawer/index.vue';
|
||||
import stepType from '../common/stepType.vue';
|
||||
import stepTree, { ScenarioStepItem } from './stepTree.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
type: ScenarioStepType;
|
||||
name: string;
|
||||
description: string;
|
||||
status: ScenarioExecuteStatus;
|
||||
children?: ScenarioStepItem[];
|
||||
}
|
||||
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
export interface ScenarioStepInfo {
|
||||
id: string | number;
|
||||
steps: ScenarioStepItem[];
|
||||
checkedAll: boolean; // 是否全选
|
||||
indeterminate: boolean; // 是否半选
|
||||
isExpand: boolean; // 是否全部展开
|
||||
executeTime: string; // 执行时间
|
||||
executeTime?: string; // 执行时间
|
||||
executeSuccessCount?: number; // 执行成功数量
|
||||
executeFailCount?: number; // 执行失败数量
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isNew?: boolean; // 是否新建
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const checkedAll = ref(false); // 是否全选
|
||||
const indeterminate = ref(false); // 是否半选
|
||||
const isExpandAll = ref(false); // 是否展开全部
|
||||
const checkedKeys = ref<string[]>([]); // 选中的key
|
||||
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
|
||||
const keyword = ref('');
|
||||
const stepInfo = ref<ScenarioStepInfo>({
|
||||
id: new Date().getTime(),
|
||||
steps: [],
|
||||
checkedAll: false,
|
||||
indeterminate: false,
|
||||
isExpand: false,
|
||||
executeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
steps: [
|
||||
{
|
||||
id: 1,
|
||||
order: 1,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
order: 1,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: 'API11',
|
||||
description: 'API11描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
order: 2,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: 'API12',
|
||||
description: 'API12描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: 2,
|
||||
checked: false,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
},
|
||||
],
|
||||
executeTime: '',
|
||||
executeSuccessCount: 0,
|
||||
executeFailCount: 0,
|
||||
});
|
||||
|
||||
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
|
||||
stepInfo.value.indeterminate = false;
|
||||
indeterminate.value = false;
|
||||
if (value) {
|
||||
stepInfo.value.checkedAll = true;
|
||||
checkedAll.value = true;
|
||||
} else {
|
||||
stepInfo.value.checkedAll = false;
|
||||
checkedAll.value = false;
|
||||
}
|
||||
stepTreeRef.value?.checkAll(checkedAll.value);
|
||||
}
|
||||
|
||||
watch(checkedKeys, (val) => {
|
||||
if (val.length === 0) {
|
||||
checkedAll.value = false;
|
||||
indeterminate.value = false;
|
||||
} else if (val.length === stepInfo.value.steps.length) {
|
||||
checkedAll.value = true;
|
||||
indeterminate.value = false;
|
||||
} else {
|
||||
checkedAll.value = false;
|
||||
indeterminate.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
function expandAllStep() {
|
||||
stepInfo.value.isExpand = !stepInfo.value.isExpand;
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
}
|
||||
|
||||
function batchEnable() {
|
||||
|
@ -204,40 +219,6 @@
|
|||
function searchStep(val: string) {
|
||||
stepInfo.value.steps = stepInfo.value.steps.filter((item) => item.name.includes(val));
|
||||
}
|
||||
|
||||
const importApiDrawerVisible = ref(false);
|
||||
|
||||
function handleActionSelect(val: ScenarioAddStepActionType) {
|
||||
switch (val) {
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
importApiDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
console.log('自定义API');
|
||||
break;
|
||||
case ScenarioAddStepActionType.LOOP_CONTROL:
|
||||
console.log('循环控制');
|
||||
break;
|
||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||
console.log('条件控制');
|
||||
break;
|
||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||
console.log('仅执行一次');
|
||||
break;
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
console.log('脚本操作');
|
||||
break;
|
||||
case ScenarioAddStepActionType.WAIT_TIME:
|
||||
console.log('等待时间');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function openTutorial() {
|
||||
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -277,17 +258,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.add-step-btn {
|
||||
@apply bg-white;
|
||||
|
||||
padding: 4px;
|
||||
border: 1px dashed rgb(var(--primary-3));
|
||||
color: rgb(var(--primary-5));
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px dashed rgb(var(--primary-5));
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<MsTree
|
||||
ref="treeRef"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:focus-node-key="focusStepKey"
|
||||
:data="props.steps"
|
||||
:node-more-actions="stepMoreActions"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:selectable="false"
|
||||
disabled-title-tooltip
|
||||
checkable
|
||||
block-node
|
||||
draggable
|
||||
>
|
||||
<template #title="step">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<div
|
||||
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
|
||||
>
|
||||
{{ step.order }}
|
||||
</div>
|
||||
<div class="step-node-first">
|
||||
<div
|
||||
v-show="step.children?.length > 0"
|
||||
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"
|
||||
@click.stop="toggleNodeExpand(step)"
|
||||
>
|
||||
<MsIcon
|
||||
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
|
||||
:size="14"
|
||||
/>
|
||||
{{ step.children?.length || 0 }}
|
||||
</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ step.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra="step">
|
||||
<MsButton :id="step.key" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="setFocusNodeKey(step)">
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<executeStatus :status="step.status" size="small" />
|
||||
</template>
|
||||
</MsTree>
|
||||
<actionDropdown
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
|
||||
>
|
||||
<a-button type="dashed" class="add-step-btn" long>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<icon-plus />
|
||||
{{ t('apiScenario.addStep') }}
|
||||
</div>
|
||||
</a-button>
|
||||
</actionDropdown>
|
||||
<importApiDrawer v-model:visible="importApiDrawerVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
import executeStatus from '../common/executeStatus.vue';
|
||||
import importApiDrawer from '../common/importApiDrawer/index.vue';
|
||||
import stepType from '../common/stepType.vue';
|
||||
import actionDropdown from './actionDropdown.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
order: number;
|
||||
checked: boolean;
|
||||
type: ScenarioStepType;
|
||||
name: string;
|
||||
description: string;
|
||||
status: ScenarioExecuteStatus;
|
||||
children?: ScenarioStepItem[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
steps: ScenarioStepItem[];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const checkedKeys = defineModel<string[]>('checkedKeys', {
|
||||
required: true,
|
||||
});
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string>(''); // 聚焦的key
|
||||
const stepMoreActions: ActionsItem[] = [
|
||||
{
|
||||
label: 'common.execute',
|
||||
eventTag: 'execute',
|
||||
},
|
||||
{
|
||||
label: 'common.copy',
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
label: 'apiScenario.scenarioConfig',
|
||||
eventTag: 'config',
|
||||
},
|
||||
{
|
||||
label: 'common.delete',
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||
focusStepKey.value = node.id || '';
|
||||
}
|
||||
|
||||
function toggleNodeExpand(node: MsTreeNodeData) {
|
||||
if (node.id) {
|
||||
treeRef.value?.expandNode(node.id, !node.expanded);
|
||||
}
|
||||
}
|
||||
|
||||
function checkAll(val: boolean) {
|
||||
treeRef.value?.checkAll(val);
|
||||
}
|
||||
|
||||
const importApiDrawerVisible = ref(false);
|
||||
|
||||
function handleActionSelect(val: ScenarioAddStepActionType) {
|
||||
switch (val) {
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
importApiDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
console.log('自定义API');
|
||||
break;
|
||||
case ScenarioAddStepActionType.LOOP_CONTROL:
|
||||
console.log('循环控制');
|
||||
break;
|
||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||
console.log('条件控制');
|
||||
break;
|
||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||
console.log('仅执行一次');
|
||||
break;
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
console.log('脚本操作');
|
||||
break;
|
||||
case ScenarioAddStepActionType.WAIT_TIME:
|
||||
console.log('等待时间');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
checkAll,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.add-step-btn {
|
||||
@apply bg-white;
|
||||
|
||||
padding: 4px;
|
||||
border: 1px dashed rgb(var(--primary-3));
|
||||
color: rgb(var(--primary-5));
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px dashed rgb(var(--primary-5));
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
// 循环生成树的左边距样式
|
||||
.loop-levels(@index, @max) when (@index <= @max) {
|
||||
:deep(.arco-tree-node[data-level='@{index}']) {
|
||||
margin-left: @index * 32px;
|
||||
}
|
||||
.loop-levels(@index + 1, @max); // 下个层级
|
||||
}
|
||||
.loop-levels(0, 99); // 最大层级
|
||||
:deep(.arco-tree-node) {
|
||||
padding: 7px 8px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-medium) !important;
|
||||
&:not(:first-child) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
.arco-tree-node-title {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
&:hover {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
.step-node-first {
|
||||
@apply flex items-center;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
&[draggable='true']:hover {
|
||||
.step-node-first {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.arco-tree-node-indent {
|
||||
@apply hidden;
|
||||
}
|
||||
.arco-tree-node-switcher {
|
||||
@apply hidden;
|
||||
}
|
||||
.arco-tree-node-drag-icon {
|
||||
@apply ml-0;
|
||||
|
||||
top: 6px;
|
||||
left: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
.arco-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
gap: 4px;
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,7 @@
|
|||
<template #first>
|
||||
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
|
||||
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]">
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" />
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" is-new />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="ScenarioCreateComposition.PARAMS" :title="t('apiScenario.params')" class="p-[16px]">
|
||||
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="scenario.params" />
|
||||
|
@ -127,6 +127,8 @@
|
|||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ApiScenarioStatus, RequestCaseStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
import type { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
|
||||
|
||||
// 组成部分异步导入
|
||||
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
||||
const params = defineAsyncComponent(() => import('../components/params.vue'));
|
||||
|
@ -144,6 +146,7 @@
|
|||
const scenario = ref<any>({
|
||||
name: '',
|
||||
moduleId: 'root',
|
||||
stepInfo: {} as ScenarioStepInfo,
|
||||
status: RequestCaseStatus.PROCESSING,
|
||||
tags: [],
|
||||
params: [],
|
||||
|
|
|
@ -98,6 +98,7 @@ export default {
|
|||
'apiScenario.case': '用例',
|
||||
'apiScenario.scenario': '场景',
|
||||
'apiScenario.sumSelected': '共选择',
|
||||
'apiScenario.scenarioConfig': '场景配置',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
Loading…
Reference in New Issue