feat(接口场景): 场景步骤 35%
This commit is contained in:
parent
7b288a30e2
commit
5674d80bb5
|
@ -27,10 +27,10 @@ import {
|
|||
import {
|
||||
ApiScenarioBatchDeleteParams,
|
||||
ApiScenarioBatchEditParams,
|
||||
ApiScenarioDetail,
|
||||
ApiScenarioGetModuleParams,
|
||||
ApiScenarioModuleUpdateParams,
|
||||
ApiScenarioPageParams,
|
||||
ApiScenarioTableItem,
|
||||
ApiScenarioUpdateDTO,
|
||||
ExecuteHistoryItem,
|
||||
ExecutePageParams,
|
||||
|
@ -81,12 +81,12 @@ export function deleteModule(id: string) {
|
|||
|
||||
// 获取接口场景列表
|
||||
export function getScenarioPage(data: ApiScenarioPageParams) {
|
||||
return MSR.post<CommonList<ApiScenarioDetail>>({ url: ScenarioPageUrl, data });
|
||||
return MSR.post<CommonList<ApiScenarioTableItem>>({ url: ScenarioPageUrl, data });
|
||||
}
|
||||
|
||||
// 获取回收站的接口场景列表
|
||||
export function getTrashScenarioPage(data: ApiScenarioPageParams) {
|
||||
return MSR.post<CommonList<ApiScenarioDetail>>({ url: ScenarioTrashPageUrl, data });
|
||||
return MSR.post<CommonList<ApiScenarioTableItem>>({ url: ScenarioTrashPageUrl, data });
|
||||
}
|
||||
|
||||
// 更新接口场景
|
||||
|
|
|
@ -33,13 +33,7 @@
|
|||
<slot name="title" v-bind="_props"></slot>
|
||||
</template>
|
||||
<template v-if="$slots['extra'] || props.nodeMoreActions" #extra="_props">
|
||||
<div
|
||||
v-if="_props.hideMoreAction !== true"
|
||||
:class="[
|
||||
'ms-tree-node-extra',
|
||||
focusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '', // TODO:通过下拉菜单的显示隐藏去控制聚焦状态似乎能有更好的性能
|
||||
]"
|
||||
>
|
||||
<div v-if="_props.hideMoreAction !== true" class="ms-tree-node-extra">
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
|
@ -110,7 +104,7 @@
|
|||
virtualListProps?: VirtualListProps; // 虚拟滚动列表的属性
|
||||
disabledTitleTooltip?: boolean; // 是否禁用标题 tooltip
|
||||
actionOnNodeClick?: 'expand'; // 点击节点时的操作
|
||||
nodeHighlightBackgroundColor?: string; // 节点高亮背景色
|
||||
nodeHighlightClass?: string; // 节点高亮背景色
|
||||
titleTooltipPosition?:
|
||||
| 'top'
|
||||
| 'tl'
|
||||
|
@ -352,10 +346,10 @@
|
|||
if (val?.toString() !== '') {
|
||||
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
|
||||
if (focusEl.value) {
|
||||
focusEl.value.style.backgroundColor = props.nodeHighlightBackgroundColor || 'rgb(var(--primary-1))';
|
||||
focusEl.value.classList.add(props.nodeHighlightClass || 'ms-tree-node-focus');
|
||||
}
|
||||
} else if (focusEl.value) {
|
||||
focusEl.value.style.backgroundColor = '';
|
||||
focusEl.value.classList.remove(props.nodeHighlightClass || 'ms-tree-node-focus');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -465,7 +459,7 @@
|
|||
width: 60%;
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply invisible relative flex w-0 items-center;
|
||||
@apply invisible relative sticky right-0 flex w-0 items-center;
|
||||
|
||||
margin-left: -4px;
|
||||
height: 32px;
|
||||
|
@ -489,13 +483,19 @@
|
|||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra--focus {
|
||||
@apply visible w-auto;
|
||||
}
|
||||
.arco-tree-node-custom-icon {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.ms-tree-node-focus {
|
||||
background-color: rgb(var(--primary-1));
|
||||
.arco-tree-node-title {
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply visible w-auto;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-selected {
|
||||
background-color: rgb(var(--primary-1));
|
||||
.arco-tree-node-minus-icon,
|
||||
|
@ -519,6 +519,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
.arco-tree-node-disabled {
|
||||
&:hover {
|
||||
|
|
|
@ -115,6 +115,7 @@ export default {
|
|||
'common.batchEdit': 'Batch Edit',
|
||||
'common.tagsInputPlaceholder': 'Enter the content and press Enter to directly add tags',
|
||||
'common.move': 'Move',
|
||||
'common.moveSuccess': 'Move successful',
|
||||
'common.batchMove': 'Batch move',
|
||||
'common.batchCopy': 'Batch copy',
|
||||
'common.batchMoveSuccess': 'Batch move successful',
|
||||
|
|
|
@ -116,6 +116,7 @@ export default {
|
|||
'common.batchEdit': '批量编辑',
|
||||
'common.tagsInputPlaceholder': '输入内容后回车可直接添加标签',
|
||||
'common.move': '移动',
|
||||
'common.moveSuccess': '移动成功',
|
||||
'common.batchMove': '批量移动',
|
||||
'common.batchCopy': '批量复制',
|
||||
'common.batchMoveSuccess': '批量移动成功',
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
|
||||
|
||||
import { ApiDefinitionCustomField } from '@/models/apiTest/management';
|
||||
import { ApiScenarioStatus, RequestComposition, RequestDefinitionStatus, RequestImportFormat } from '@/enums/apiEnum';
|
||||
import { ApiScenarioStatus, RequestComposition, RequestDefinitionStatus } from '@/enums/apiEnum';
|
||||
|
||||
import { BatchApiParams, TableQueryParams } from '../common';
|
||||
import { ExecuteApiRequestFullParams, ResponseDefinition } from './common';
|
||||
|
@ -40,7 +42,7 @@ export interface ApiScenarioUpdateDTO {
|
|||
}
|
||||
|
||||
// 场景详情
|
||||
export interface ApiScenarioDetail {
|
||||
export interface ApiScenarioTableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
method: string;
|
||||
|
@ -177,3 +179,25 @@ export type CustomApiStep = ExecuteApiRequestFullParams & {
|
|||
activeTab: RequestComposition;
|
||||
useEnv: string;
|
||||
};
|
||||
// 场景步骤-循环控制器类型
|
||||
export type ScenarioStepLoopType = 'num' | 'while' | 'forEach';
|
||||
// 场景步骤-循环控制器-循环类型
|
||||
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
|
||||
// 场景步骤-步骤插入类型
|
||||
export type CreateStepAction = 'addChildStep' | 'insertBefore' | 'insertAfter' | undefined;
|
||||
// 场景步骤
|
||||
export interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
moduleId: string | number;
|
||||
stepInfo: ScenarioStepInfo;
|
||||
status: RequestDefinitionStatus;
|
||||
tags: string[];
|
||||
params: Record<string, any>[];
|
||||
// 前端渲染字段
|
||||
label: string;
|
||||
closable: boolean;
|
||||
isNew: boolean;
|
||||
unSaved: boolean;
|
||||
executeLoading: boolean; // 执行loading
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
import { BatchActionQueryParams, MsTableColumnData } from '@/components/pure/ms-table/type';
|
||||
|
@ -6,7 +7,6 @@ import { BugEditCustomField, CustomFieldItem } from '@/models/bug-management';
|
|||
import type { CustomAttributes } from '@/models/caseManagement/featureCase';
|
||||
|
||||
import { isObject } from './is';
|
||||
import { json } from 'stream/consumers';
|
||||
|
||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||
|
||||
|
@ -194,6 +194,9 @@ export interface TreeNode<T> {
|
|||
* @param tree 树形数组或树
|
||||
* @param customNodeFn 自定义节点函数
|
||||
* @param customChildrenKey 自定义子节点的key
|
||||
* @param parent 父节点
|
||||
* @param parentPath 父节点路径
|
||||
* @param level 节点层级
|
||||
* @returns 遍历后的树形数组
|
||||
*/
|
||||
export function mapTree<T>(
|
||||
|
@ -201,33 +204,40 @@ export function mapTree<T>(
|
|||
customNodeFn: (node: TreeNode<T>, path: string) => TreeNode<T> | null = (node) => node,
|
||||
customChildrenKey = 'children',
|
||||
parentPath = '',
|
||||
level = 0
|
||||
level = 0,
|
||||
parent: TreeNode<T> | null = null
|
||||
): T[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
tree = [tree];
|
||||
let cloneTree = cloneDeep(tree);
|
||||
if (!Array.isArray(cloneTree)) {
|
||||
cloneTree = [cloneTree];
|
||||
}
|
||||
|
||||
return tree
|
||||
.map((node: TreeNode<T>) => {
|
||||
const fullPath = node.path ? `${parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
|
||||
function mapFunc(
|
||||
_tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
_parentPath = '',
|
||||
_level = 0,
|
||||
_parent: TreeNode<T> | null = null
|
||||
): T[] {
|
||||
if (!Array.isArray(_tree)) {
|
||||
_tree = [_tree];
|
||||
}
|
||||
return _tree
|
||||
.map((node: TreeNode<T>, i: number) => {
|
||||
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
|
||||
node.order = i + 1; // order从 1 开始
|
||||
node.parent = _parent || undefined; // 没有父节点说明是树的第一层
|
||||
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
|
||||
|
||||
if (newNode) {
|
||||
newNode.level = level;
|
||||
newNode.level = _level;
|
||||
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
|
||||
newNode[customChildrenKey] = mapTree(
|
||||
newNode[customChildrenKey],
|
||||
customNodeFn,
|
||||
customChildrenKey,
|
||||
fullPath,
|
||||
level + 1
|
||||
);
|
||||
newNode[customChildrenKey] = mapFunc(newNode[customChildrenKey], fullPath, _level + 1, node);
|
||||
}
|
||||
}
|
||||
|
||||
return newNode;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
return mapFunc(cloneTree, parentPath, level, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -347,7 +357,7 @@ export function findNodePathByKey<T>(
|
|||
return null;
|
||||
}
|
||||
/**
|
||||
* 在某个节点前插入新节点
|
||||
* 在某个节点前/后插入新节点
|
||||
* @param treeArr 目标树
|
||||
* @param targetKey 目标节点唯一值
|
||||
* @param newNode 新节点
|
||||
|
@ -356,22 +366,49 @@ export function findNodePathByKey<T>(
|
|||
*/
|
||||
export function insertNode<T>(
|
||||
treeArr: TreeNode<T>[],
|
||||
targetKey: string,
|
||||
targetKey: string | number,
|
||||
newNode: TreeNode<T>,
|
||||
position: 'before' | 'after',
|
||||
position: 'before' | 'after' | 'inside',
|
||||
customFunc?: (node: TreeNode<T>, parent?: TreeNode<T>) => void,
|
||||
customKey = 'key'
|
||||
): void {
|
||||
function insertNodeInTree(tree: TreeNode<T>[], parent?: TreeNode<T>): boolean {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (node[customKey] === targetKey) {
|
||||
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后插入新节点
|
||||
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后/内部插入新节点
|
||||
const childrenArray = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
|
||||
const index = childrenArray.findIndex((item) => item[customKey] === node[customKey]);
|
||||
if (position === 'before') {
|
||||
newNode.parent = parent || node.parent;
|
||||
newNode.order = node.order;
|
||||
childrenArray.splice(index, 0, newNode);
|
||||
for (let j = index + 1; j < childrenArray.length; j++) {
|
||||
// 更新插入节点之后的节点的 order
|
||||
if (childrenArray[j].order !== undefined) {
|
||||
childrenArray[j].order += 1;
|
||||
}
|
||||
}
|
||||
} else if (position === 'after') {
|
||||
newNode.parent = parent || node.parent;
|
||||
newNode.order = node.order + 1;
|
||||
childrenArray.splice(index + 1, 0, newNode);
|
||||
// 更新插入节点之后的节点的 order
|
||||
for (let j = index + 2; j < childrenArray.length; j++) {
|
||||
if (childrenArray[j].order !== undefined) {
|
||||
childrenArray[j].order += 1;
|
||||
}
|
||||
}
|
||||
} else if (position === 'inside') {
|
||||
if (!node.children) {
|
||||
node.children = [];
|
||||
}
|
||||
newNode.parent = node;
|
||||
newNode.order = node.children.length + 1;
|
||||
node.children.push(newNode);
|
||||
}
|
||||
if (typeof customFunc === 'function') {
|
||||
customFunc(newNode, parent);
|
||||
}
|
||||
// 插入后返回 true
|
||||
return true;
|
||||
|
@ -386,6 +423,54 @@ export function insertNode<T>(
|
|||
insertNodeInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理树结构之间拖拽节点
|
||||
* @param treeArr 整颗树
|
||||
* @param dragNode 拖动节点
|
||||
* @param dropNode 目标节点
|
||||
* @param dropPosition -1: before, 0: inside, 1: after
|
||||
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
||||
*/
|
||||
export function handleTreeDragDrop<T>(
|
||||
treeArr: TreeNode<T>[],
|
||||
dragNode: TreeNode<T>,
|
||||
dropNode: TreeNode<T>,
|
||||
dropPosition: number,
|
||||
customKey = 'key'
|
||||
): boolean {
|
||||
// 把 dragNode 从原来的位置删除
|
||||
const parentChildren = dragNode.parent?.children || treeArr;
|
||||
if (dragNode.parent?.[customKey] === dropNode[customKey] && dropPosition === 0) {
|
||||
// 如果拖动的节点释放到自己的父节点上,不做任何操作
|
||||
return false;
|
||||
}
|
||||
const index = parentChildren.findIndex((node: TreeNode<T>) => node[customKey] === dragNode[customKey]);
|
||||
if (index !== -1) {
|
||||
parentChildren.splice(index, 1);
|
||||
|
||||
// 更新删除节点后的节点的 order
|
||||
for (let i = index; i < parentChildren.length; i++) {
|
||||
parentChildren[i].order -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 拖动节点插入到目标节点的 children 数组中
|
||||
if (dropPosition === 0) {
|
||||
insertNode(dropNode.parent?.children || treeArr, dropNode[customKey], dragNode, 'inside', undefined, customKey);
|
||||
} else {
|
||||
// 拖动节点插入到目标节点的前/后
|
||||
insertNode(
|
||||
dropNode.parent?.children || treeArr,
|
||||
dropNode[customKey],
|
||||
dragNode,
|
||||
dropPosition === -1 ? 'before' : 'after',
|
||||
undefined,
|
||||
customKey
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除树形数组中的某个节点
|
||||
* @param treeArr 目标树
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
interface Tree {
|
||||
id: number;
|
||||
id: string | number;
|
||||
groupId?: number;
|
||||
children?: Tree[];
|
||||
[key: string]: any;
|
||||
|
|
|
@ -459,7 +459,7 @@
|
|||
>
|
||||
<template #rightTitle>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-[var(--color-text-1)]">
|
||||
<div class="text-[var(--color-text-4)]">
|
||||
{{ t('apiTestDebug.quickInputParamsTip') }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -103,7 +103,8 @@
|
|||
<!-- 接口定义-调试模式,可保存或保存为新用例 -->
|
||||
<a-dropdown
|
||||
v-if="requestVModel.mode === 'debug'"
|
||||
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
:loading="saveLoading"
|
||||
:disabled="isHttpProtocol && !requestVModel.url"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<a-button type="secondary">
|
||||
|
@ -1401,7 +1402,7 @@
|
|||
}
|
||||
|
||||
// 保存为用例
|
||||
function saveAsCase() {
|
||||
function saveAsCase(done: (closed: boolean) => void) {
|
||||
saveCaseModalFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
|
@ -1421,12 +1422,14 @@
|
|||
};
|
||||
await addCase(params);
|
||||
emit('addDone');
|
||||
done(true);
|
||||
Message.success(t('common.saveSuccess'));
|
||||
saveCaseModalVisible.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
done(false);
|
||||
} finally {
|
||||
saveCaseLoading.value = false;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export default {
|
|||
'apiTestDebug.batchAddParamsTip1': '书写格式:参数名:参数值;如 nama:natural,多条记录以换行分隔',
|
||||
'apiTestDebug.batchAddParamsTip2': '默认添加为 string 类型;file 类型参数无法在此编辑',
|
||||
'apiTestDebug.batchAddParamsTip3': '批量添加里的参数名重复,默认以最后一条数据为最新数据',
|
||||
'apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
|
||||
'apiTestDebug.quickInputParamsTip': '支持 Mock/JMeter/Json/Text/String 等',
|
||||
'apiTestDebug.descPlaceholder': '请输入内容',
|
||||
'apiTestDebug.noneBody': '请求没有 Body',
|
||||
'apiTestDebug.sendAsMainText': '作为正文发送',
|
||||
|
|
|
@ -110,7 +110,9 @@
|
|||
</div>
|
||||
<div v-else :id="nodeData.id" class="inline-flex w-full">
|
||||
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||
<div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||
<div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]"
|
||||
>({{ modulesCount[nodeData.id] || 0 }})</div
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!props.readOnly && !props.isModal" #extra="nodeData">
|
||||
|
@ -450,9 +452,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleProtocolChange() {
|
||||
function handleProtocolChange() {
|
||||
emit('changeProtocol', moduleProtocol.value);
|
||||
await initModules();
|
||||
initModules();
|
||||
initModuleCount();
|
||||
}
|
||||
|
||||
|
@ -467,9 +469,9 @@
|
|||
isExpandAll.value = !isExpandAll.value;
|
||||
}
|
||||
|
||||
async function changeApiExpand() {
|
||||
function changeApiExpand() {
|
||||
isExpandApi.value = !isExpandApi.value;
|
||||
await initModules();
|
||||
initModules();
|
||||
initModuleCount();
|
||||
}
|
||||
|
||||
|
@ -515,7 +517,7 @@
|
|||
}
|
||||
Message.success(t('apiTestDebug.deleteSuccess'));
|
||||
emit('deleteNode', node.id, node.type === 'MODULE');
|
||||
await initModules();
|
||||
initModules();
|
||||
initModuleCount();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -629,8 +631,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleAddFinish() {
|
||||
await initModules();
|
||||
function handleAddFinish() {
|
||||
initModules();
|
||||
initModuleCount();
|
||||
}
|
||||
|
||||
|
@ -646,14 +648,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
onBeforeMount(() => {
|
||||
initProtocolList();
|
||||
await initModules();
|
||||
initModules();
|
||||
initModuleCount();
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
await initModules();
|
||||
function refresh() {
|
||||
initModules();
|
||||
initModuleCount();
|
||||
}
|
||||
|
||||
|
|
|
@ -184,7 +184,10 @@
|
|||
() => visible.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// 外面使用 v-if 动态渲染时,需要在下一个tick中初始化
|
||||
nextTick(() => {
|
||||
resetModuleAndTable();
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
|
||||
class="text-nowrap rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
|
||||
:style="{
|
||||
borderColor: type.color,
|
||||
color: type.color,
|
||||
|
|
|
@ -1,7 +1,63 @@
|
|||
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
|
||||
|
||||
export const defaultStepItemCommon = {
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
children: [],
|
||||
loopNum: 0,
|
||||
loopType: 'num' as ScenarioStepLoopType,
|
||||
loopSpace: 0,
|
||||
variableName: '',
|
||||
variablePrefix: '',
|
||||
loopWhileType: 'condition' as ScenarioStepLoopWhileType,
|
||||
variableVal: '',
|
||||
condition: 'equal',
|
||||
overTime: 0,
|
||||
expression: '',
|
||||
waitTime: 0,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default {};
|
||||
export const conditionOptions = [
|
||||
{
|
||||
value: 'equal',
|
||||
label: 'apiScenario.equal',
|
||||
},
|
||||
{
|
||||
value: 'notEqualTo',
|
||||
label: 'apiScenario.notEqualTo',
|
||||
},
|
||||
{
|
||||
value: 'greater',
|
||||
label: 'apiScenario.greater',
|
||||
},
|
||||
{
|
||||
value: 'less',
|
||||
label: 'apiScenario.less',
|
||||
},
|
||||
{
|
||||
value: 'greaterOrEqual',
|
||||
label: 'apiScenario.greaterOrEqual',
|
||||
},
|
||||
{
|
||||
value: 'lessOrEqual',
|
||||
label: 'apiScenario.lessOrEqual',
|
||||
},
|
||||
{
|
||||
value: 'include',
|
||||
label: 'apiScenario.include',
|
||||
},
|
||||
{
|
||||
value: 'notInclude',
|
||||
label: 'apiScenario.notInclude',
|
||||
},
|
||||
{
|
||||
value: 'null',
|
||||
label: 'apiScenario.null',
|
||||
},
|
||||
{
|
||||
value: 'notNull',
|
||||
label: 'apiScenario.notNull',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -270,7 +270,7 @@
|
|||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ApiScenarioDetail, ApiScenarioUpdateDTO } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioTableItem, ApiScenarioUpdateDTO } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioStatus } from '@/enums/apiEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
|
@ -563,7 +563,7 @@
|
|||
/**
|
||||
* 删除接口
|
||||
*/
|
||||
function deleteScenario(record?: ApiScenarioDetail, isBatch?: boolean, params?: BatchActionQueryParams) {
|
||||
function deleteScenario(record?: ApiScenarioTableItem, isBatch?: boolean, params?: BatchActionQueryParams) {
|
||||
let title = t('api_scenario.table.deleteScenarioTipTitle', { name: record?.name });
|
||||
let selectIds = [record?.id || ''];
|
||||
if (isBatch) {
|
||||
|
@ -613,7 +613,7 @@
|
|||
* 处理表格更多按钮事件
|
||||
* @param item
|
||||
*/
|
||||
function handleTableMoreActionSelect(item: ActionsItem, record: ApiScenarioDetail) {
|
||||
function handleTableMoreActionSelect(item: ActionsItem, record: ApiScenarioTableItem) {
|
||||
switch (item.eventTag) {
|
||||
case 'delete':
|
||||
deleteScenario(record);
|
||||
|
@ -734,7 +734,7 @@
|
|||
const selectedBatchOptModuleName = ref(''); // 移动文件选中节点 用于页面文案显示
|
||||
const batchOptionType = ref(''); // 批量操作类型 用于页面提示语
|
||||
const batchOptionScenarioCount = ref<number>(0);
|
||||
const activeScenario = ref<ApiScenarioDetail | null>(null); // 当前查看的接口项
|
||||
const activeScenario = ref<ApiScenarioTableItem | null>(null); // 当前查看的接口项
|
||||
const scenarioBatchOptTreeLoading = ref(false); // 批量移动文件loading
|
||||
|
||||
/**
|
||||
|
@ -834,7 +834,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function openScenarioTab(record: ApiScenarioDetail) {
|
||||
function openScenarioTab(record: ApiScenarioTableItem) {
|
||||
Message.info('// todo @ba1q1');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
<template>
|
||||
<a-dropdown
|
||||
v-model:popup-visible="visible"
|
||||
:position="props.position || 'bottom'"
|
||||
:popup-translate="props.popupTranslate"
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => emit('select', val as ScenarioAddStepActionType)"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #content>
|
||||
<a-dgroup :title="t('apiScenario.requestScenario')">
|
||||
<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 { TriggerPopupTranslate } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioAddStepActionType } from '@/enums/apiEnum';
|
||||
|
||||
import { DropdownPosition } from '@arco-design/web-vue/es/dropdown/interface';
|
||||
|
||||
const props = defineProps<{
|
||||
position?: DropdownPosition;
|
||||
popupTranslate?: TriggerPopupTranslate;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', val: ScenarioAddStepActionType): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
function openTutorial() {
|
||||
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,260 @@
|
|||
<template>
|
||||
<a-dropdown
|
||||
v-model:popup-visible="visible"
|
||||
:position="props.position || 'bottom'"
|
||||
:popup-translate="props.popupTranslate"
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleCreateActionSelect(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 { TriggerPopupTranslate } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import { ScenarioStepItem } from '../stepTree.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { findNodeByKey, getGenerateId, insertNode, TreeNode } from '@/utils';
|
||||
|
||||
import { CreateStepAction } from '@/models/apiTest/scenario';
|
||||
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
|
||||
import { DropdownPosition } from '@arco-design/web-vue/es/dropdown/interface';
|
||||
|
||||
const props = defineProps<{
|
||||
position?: DropdownPosition;
|
||||
popupTranslate?: TriggerPopupTranslate;
|
||||
createStepAction?: CreateStepAction;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'close');
|
||||
(
|
||||
e: 'otherCreate',
|
||||
type:
|
||||
| ScenarioAddStepActionType.IMPORT_SYSTEM_API
|
||||
| ScenarioAddStepActionType.CUSTOM_API
|
||||
| ScenarioAddStepActionType.SCRIPT_OPERATION,
|
||||
step?: ScenarioStepItem
|
||||
);
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
const steps = defineModel<ScenarioStepItem[]>('steps', {
|
||||
required: true,
|
||||
});
|
||||
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
|
||||
required: true,
|
||||
});
|
||||
const step = defineModel<ScenarioStepItem>('step', {
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||
*/
|
||||
function isParentSelected(parent?: TreeNode<ScenarioStepItem>) {
|
||||
if (parent && selectedKeys.value.includes(parent.id)) {
|
||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(step.value.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理添加子步骤、插入步骤前/后操作
|
||||
*/
|
||||
function handleCreateStep(defaultStepInfo: ScenarioStepItem) {
|
||||
switch (props.createStepAction) {
|
||||
case 'addChildStep':
|
||||
const id = getGenerateId();
|
||||
if (step.value?.children) {
|
||||
step.value.children.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...defaultStepInfo,
|
||||
id,
|
||||
order: step.value.children.length + 1,
|
||||
});
|
||||
} else {
|
||||
step.value.children = [
|
||||
{
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...defaultStepInfo,
|
||||
id,
|
||||
order: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (selectedKeys.value.includes(step.value.id)) {
|
||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(id);
|
||||
}
|
||||
break;
|
||||
case 'insertBefore':
|
||||
insertNode<ScenarioStepItem>(
|
||||
step.value.children || steps.value,
|
||||
step.value.id,
|
||||
{
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...defaultStepInfo,
|
||||
id: getGenerateId(),
|
||||
order: step.value.order,
|
||||
},
|
||||
'before',
|
||||
isParentSelected,
|
||||
'id'
|
||||
);
|
||||
break;
|
||||
case 'insertAfter':
|
||||
insertNode<ScenarioStepItem>(
|
||||
step.value.children || steps.value,
|
||||
step.value.id,
|
||||
{
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...defaultStepInfo,
|
||||
id: getGenerateId(),
|
||||
order: step.value.order + 1,
|
||||
},
|
||||
'after',
|
||||
isParentSelected,
|
||||
'id'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建步骤操作
|
||||
* @param val 创建步骤类型
|
||||
*/
|
||||
function handleCreateActionSelect(val: ScenarioAddStepActionType) {
|
||||
switch (val) {
|
||||
case ScenarioAddStepActionType.LOOP_CONTROL:
|
||||
if (step.value) {
|
||||
handleCreateStep({
|
||||
type: ScenarioStepType.LOOP_CONTROL,
|
||||
name: t('apiScenario.loopControl'),
|
||||
} as ScenarioStepItem);
|
||||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.LOOP_CONTROL,
|
||||
name: t('apiScenario.loopControl'),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||
if (step.value) {
|
||||
handleCreateStep({
|
||||
type: ScenarioStepType.CONDITION_CONTROL,
|
||||
name: t('apiScenario.conditionControl'),
|
||||
} as ScenarioStepItem);
|
||||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.CONDITION_CONTROL,
|
||||
name: t('apiScenario.conditionControl'),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||
if (step.value) {
|
||||
handleCreateStep({
|
||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
name: t('apiScenario.onlyOnceControl'),
|
||||
} as ScenarioStepItem);
|
||||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
name: t('apiScenario.onlyOnceControl'),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ScenarioAddStepActionType.WAIT_TIME:
|
||||
if (step.value) {
|
||||
handleCreateStep({
|
||||
type: ScenarioStepType.WAIT_TIME,
|
||||
name: t('apiScenario.waitTime'),
|
||||
} as ScenarioStepItem);
|
||||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.WAIT_TIME,
|
||||
name: t('apiScenario.waitTime'),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
if (step.value) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.id, 'id');
|
||||
if (realStep) {
|
||||
emit('otherCreate', val, realStep as ScenarioStepItem);
|
||||
}
|
||||
} else {
|
||||
emit('otherCreate', val);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (step.value) {
|
||||
document.getElementById(step.value.id.toString())?.click();
|
||||
}
|
||||
}
|
||||
|
||||
function openTutorial() {
|
||||
window.open('https://zhuanlan.zhihu.com/p/597905464?utm_id=0', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<a-trigger
|
||||
trigger="click"
|
||||
class="arco-trigger-menu absolute"
|
||||
content-class="w-[160px]"
|
||||
position="br"
|
||||
@popup-visible-change="handleActionTriggerChange"
|
||||
>
|
||||
<MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<createStepActions
|
||||
v-model:visible="innerStep.actionDropdownVisible"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="steps"
|
||||
v-model:step="innerStep"
|
||||
:create-step-action="activeCreateAction"
|
||||
position="br"
|
||||
:popup-translate="[-7, -10]"
|
||||
@other-create="(type, step) => emit('otherCreate', type, step)"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<span></span>
|
||||
</createStepActions>
|
||||
<div class="arco-trigger-menu-inner">
|
||||
<div
|
||||
v-if="showAddChildStep"
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeCreateAction === 'addChildStep' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick('addChildStep')"
|
||||
>
|
||||
<icon-plus size="12" />
|
||||
{{ t('apiScenario.addChildStep') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeCreateAction === 'insertBefore' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick('insertBefore')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertBefore') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeCreateAction === 'insertAfter' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick('insertAfter')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertAfter') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-trigger>
|
||||
</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 { ScenarioStepItem } from '../stepTree.vue';
|
||||
import createStepActions from './createStepActions.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { CreateStepAction } from '@/models/apiTest/scenario';
|
||||
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
step: ScenarioStepItem;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'close');
|
||||
(e: 'click');
|
||||
(
|
||||
e: 'otherCreate',
|
||||
type:
|
||||
| ScenarioAddStepActionType.IMPORT_SYSTEM_API
|
||||
| ScenarioAddStepActionType.CUSTOM_API
|
||||
| ScenarioAddStepActionType.SCRIPT_OPERATION,
|
||||
step?: ScenarioStepItem
|
||||
);
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const steps = defineModel<ScenarioStepItem[]>('steps', {
|
||||
required: true,
|
||||
});
|
||||
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
|
||||
required: true,
|
||||
});
|
||||
const innerStep = ref<ScenarioStepItem>(props.step);
|
||||
|
||||
watch(
|
||||
() => props.step,
|
||||
(val) => {
|
||||
innerStep.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
const showAddChildStep = computed(() => {
|
||||
return [
|
||||
ScenarioStepType.LOOP_CONTROL,
|
||||
ScenarioStepType.CONDITION_CONTROL,
|
||||
ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
ScenarioStepType.COPY_SCENARIO,
|
||||
].includes(innerStep.value.type);
|
||||
});
|
||||
|
||||
const activeCreateAction = ref<CreateStepAction>();
|
||||
function handleTriggerActionClick(action: CreateStepAction) {
|
||||
innerStep.value.actionDropdownVisible = true;
|
||||
activeCreateAction.value = action;
|
||||
}
|
||||
|
||||
function handleActionTriggerChange(val: boolean) {
|
||||
if (!val) {
|
||||
// 关闭操作下拉时,清空操作状态 TODO:部分操作是打开抽屉,需要特殊处理
|
||||
activeCreateAction.value = undefined;
|
||||
innerStep.value.actionDropdownVisible = false;
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -10,7 +10,7 @@
|
|||
/>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.sum') }}
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ stepInfo.steps.length }}</div>
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div>
|
||||
{{ t('apiScenario.steps') }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,19 +89,37 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:visible="batchToggleVisible"
|
||||
:title="isBatchEnable ? t('common.batchEnable') : t('common.batchDisable')"
|
||||
:width="480"
|
||||
:ok-text="isBatchEnable ? t('common.enable') : t('common.disable')"
|
||||
class="ms-modal-form"
|
||||
title-align="start"
|
||||
body-class="!p-0"
|
||||
@close="resetBatchToggle"
|
||||
@before-ok="handleBeforeBatchToggle"
|
||||
>
|
||||
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiScenario.range') }}</div>
|
||||
<a-radio-group v-model:model-value="batchToggleRange">
|
||||
<a-radio value="top">{{ t('apiScenario.topStep') }}</a-radio>
|
||||
<a-radio value="all">{{ t('apiScenario.allStep') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import stepTree, { ScenarioStepItem } from './stepTree.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { RequestMethods, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { countNodes } from '@/utils/tree';
|
||||
|
||||
export interface ScenarioStepInfo {
|
||||
id: string | number;
|
||||
|
@ -118,91 +136,18 @@
|
|||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const stepInfo = defineModel<ScenarioStepInfo>('step', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const checkedAll = ref(false); // 是否全选
|
||||
const indeterminate = ref(false); // 是否半选
|
||||
const isExpandAll = ref(false); // 是否展开全部
|
||||
const checkedKeys = ref<string[]>([]); // 选中的key
|
||||
const checkedKeys = ref<(string | number)[]>([]); // 选中的key
|
||||
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
|
||||
const keyword = ref('');
|
||||
const stepInfo = ref<ScenarioStepInfo>({
|
||||
id: new Date().getTime(),
|
||||
steps: [
|
||||
{
|
||||
id: 1,
|
||||
num: 10086,
|
||||
order: 1,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_API,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
method: RequestMethods.GET,
|
||||
belongProjectId: appStore.currentProjectId,
|
||||
belongProjectName: '项目名称',
|
||||
actionDropdownVisible: false,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
order: 1,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_CASE,
|
||||
name: 'API11',
|
||||
description: 'API11描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
num: 100861,
|
||||
belongProjectId: '989d23d23d',
|
||||
belongProjectName: '项目名称1',
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
order: 2,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.QUOTE_SCENARIO,
|
||||
name: 'API12',
|
||||
description: 'API12描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
num: 100862,
|
||||
belongProjectId: '989d23d23d',
|
||||
belongProjectName: '项目名称2',
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: 2,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.LOOP_CONTROL,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: 3,
|
||||
checked: false,
|
||||
expanded: false,
|
||||
enabled: true,
|
||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
name: 'API1',
|
||||
description: 'API1描述',
|
||||
status: ScenarioExecuteStatus.SUCCESS,
|
||||
actionDropdownVisible: false,
|
||||
},
|
||||
],
|
||||
executeTime: '',
|
||||
executeSuccessCount: 0,
|
||||
executeFailCount: 0,
|
||||
});
|
||||
|
||||
const totalStepCount = computed(() => countNodes(stepInfo.value.steps));
|
||||
|
||||
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
|
||||
indeterminate.value = false;
|
||||
|
@ -231,12 +176,44 @@
|
|||
isExpandAll.value = !isExpandAll.value;
|
||||
}
|
||||
|
||||
// 批量启用/禁用
|
||||
const isBatchEnable = ref(false);
|
||||
const batchToggleVisible = ref(false);
|
||||
const batchToggleRange = ref('top');
|
||||
function batchEnable() {
|
||||
console.log('批量启用');
|
||||
batchToggleVisible.value = true;
|
||||
isBatchEnable.value = true;
|
||||
}
|
||||
|
||||
function batchDisable() {
|
||||
console.log('批量禁用');
|
||||
batchToggleVisible.value = true;
|
||||
isBatchEnable.value = false;
|
||||
}
|
||||
|
||||
function resetBatchToggle() {
|
||||
batchToggleVisible.value = false;
|
||||
batchToggleRange.value = 'top';
|
||||
isBatchEnable.value = false;
|
||||
}
|
||||
|
||||
async function handleBeforeBatchToggle(done: (closed: boolean) => void) {
|
||||
try {
|
||||
let ids = checkedKeys.value;
|
||||
if (batchToggleRange.value === 'top') {
|
||||
ids = stepInfo.value.steps.map((item) => item.id);
|
||||
}
|
||||
console.log('ids', ids);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, 1000);
|
||||
});
|
||||
done(true);
|
||||
Message.success(isBatchEnable.value ? t('common.enableSuccess') : t('common.disableSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function batchDebug() {
|
||||
|
|
|
@ -1,7 +1,91 @@
|
|||
<template>
|
||||
<div>condition </div>
|
||||
<div class="flex items-center gap-[4px]" draggable="false">
|
||||
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
|
||||
<a-input
|
||||
v-model:model-value="innerData.variableName"
|
||||
size="mini"
|
||||
class="w-[100px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
<a-select
|
||||
v-model:model-value="innerData.condition"
|
||||
size="mini"
|
||||
class="w-[90px] px-[8px]"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
|
||||
{{ t(opt.label) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
v-model:model-value="innerData.variableVal"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.variableVal')"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { conditionOptions } from '@/views/api-test/scenario/components/config';
|
||||
|
||||
export interface ConditionContentProps {
|
||||
id: string;
|
||||
variableName: string;
|
||||
condition: string;
|
||||
variableVal: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: ConditionContentProps;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: ConditionContentProps): void;
|
||||
(e: 'quickInput', dataKey: keyof ConditionContentProps): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerData = ref(props.data);
|
||||
|
||||
watchEffect(() => {
|
||||
innerData.value = props.data;
|
||||
});
|
||||
|
||||
// 接收全局双击时间戳
|
||||
const dbClick = inject<
|
||||
Ref<{
|
||||
e: MouseEvent | null;
|
||||
timeStamp: number;
|
||||
}>
|
||||
>('dbClick');
|
||||
|
||||
watch(
|
||||
() => dbClick?.value.timeStamp,
|
||||
() => {
|
||||
// @ts-ignore
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
|
||||
emit('quickInput', 'variableVal');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleInputChange() {
|
||||
nextTick(() => {
|
||||
emit('change', innerData.value);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -1,7 +1,243 @@
|
|||
<template>
|
||||
<div>loop </div>
|
||||
<div class="flex items-center gap-[4px]" draggable="false">
|
||||
<a-input-group>
|
||||
<a-select
|
||||
v-model:model-value="innerData.loopType"
|
||||
:options="loopOptions"
|
||||
size="mini"
|
||||
class="w-[85px] px-[8px]"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
<a-tooltip
|
||||
v-if="innerData.loopType === 'num'"
|
||||
:content="innerData.loopNum.toString()"
|
||||
:disabled="!innerData.loopNum"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:model-value="innerData.loopNum"
|
||||
class="w-[80px] px-[8px]"
|
||||
size="mini"
|
||||
:step="1"
|
||||
:min="0"
|
||||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.num') }}:</div>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-tooltip>
|
||||
</a-input-group>
|
||||
<template v-if="innerData.loopType === 'forEach'">
|
||||
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
|
||||
<a-input
|
||||
v-model:model-value="innerData.variableName"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variableName')"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
<div class="font-medium">in</div>
|
||||
<a-tooltip :content="innerData.variablePrefix" :disabled="!innerData.variablePrefix">
|
||||
<a-input
|
||||
v-model:model-value="innerData.variablePrefix"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.variablePrefix')"
|
||||
:max-length="255"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="innerData.loopType === 'while'">
|
||||
<a-select
|
||||
v-model:model-value="innerData.loopWhileType"
|
||||
:options="whileOptions"
|
||||
size="mini"
|
||||
class="w-[75px] px-[8px]"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
<template v-if="innerData.loopWhileType === 'condition'">
|
||||
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
|
||||
<a-input
|
||||
v-model:model-value="innerData.variableName"
|
||||
size="mini"
|
||||
class="w-[100px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
<a-select
|
||||
v-model:model-value="innerData.condition"
|
||||
size="mini"
|
||||
class="w-[90px] px-[8px]"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
|
||||
{{ t(opt.label) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
v-model:model-value="innerData.variableVal"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.variableVal')"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
v-model:model-value="innerData.expression"
|
||||
size="mini"
|
||||
class="w-[200px] px-[8px]"
|
||||
:placeholder="t('apiScenario.expression')"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="innerData.overTime.toString()" :disabled="!innerData.overTime">
|
||||
<a-input-number
|
||||
v-model:model-value="innerData.overTime"
|
||||
class="w-[100px] px-[8px]"
|
||||
size="mini"
|
||||
:step="1"
|
||||
:min="0"
|
||||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.overTime') }}:</div>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip
|
||||
v-if="innerData.loopType !== 'while'"
|
||||
:content="innerData.loopSpace.toString()"
|
||||
:disabled="!innerData.loopSpace"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:model-value="innerData.loopSpace"
|
||||
size="mini"
|
||||
:step="1"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
hide-button
|
||||
class="w-[110px] px-[8px]"
|
||||
model-event="input"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.space') }}:</div>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
|
||||
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
import { conditionOptions } from '@/views/api-test/scenario/components/config';
|
||||
|
||||
export interface LoopContentProps {
|
||||
id: string | number;
|
||||
num: number;
|
||||
name: string;
|
||||
type: ScenarioStepType;
|
||||
loopNum: number;
|
||||
loopType: ScenarioStepLoopType;
|
||||
loopSpace: number;
|
||||
variableName: string;
|
||||
variablePrefix: string;
|
||||
loopWhileType: ScenarioStepLoopWhileType;
|
||||
variableVal: string;
|
||||
condition: string;
|
||||
overTime: number;
|
||||
expression: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: LoopContentProps;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: LoopContentProps): void;
|
||||
(e: 'quickInput', dataKey: keyof LoopContentProps): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerData = ref(props.data);
|
||||
const loopOptions = [
|
||||
{
|
||||
value: 'num',
|
||||
label: t('apiScenario.num'),
|
||||
},
|
||||
{
|
||||
value: 'while',
|
||||
label: 'while',
|
||||
},
|
||||
{
|
||||
value: 'forEach',
|
||||
label: 'forEach',
|
||||
},
|
||||
];
|
||||
const whileOptions = [
|
||||
{
|
||||
value: 'condition',
|
||||
label: t('apiScenario.condition'),
|
||||
},
|
||||
{
|
||||
value: 'expression',
|
||||
label: t('apiScenario.expression'),
|
||||
},
|
||||
];
|
||||
|
||||
watchEffect(() => {
|
||||
innerData.value = props.data;
|
||||
});
|
||||
|
||||
// 接收全局双击时间戳
|
||||
const dbClick = inject<
|
||||
Ref<{
|
||||
e: MouseEvent | null;
|
||||
timeStamp: number;
|
||||
}>
|
||||
>('dbClick');
|
||||
|
||||
watch(
|
||||
() => dbClick?.value.timeStamp,
|
||||
() => {
|
||||
// @ts-ignore
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
|
||||
emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleInputChange() {
|
||||
nextTick(() => {
|
||||
emit('change', innerData.value);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<template>
|
||||
<div>only once </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -1,7 +1,53 @@
|
|||
<template>
|
||||
<div>waitTime </div>
|
||||
<div class="flex items-center gap-[4px]" draggable="false">
|
||||
<a-tooltip :content="innerData.waitTime.toString()" :disabled="!innerData.waitTime">
|
||||
<a-input-number
|
||||
v-model:model-value="innerData.waitTime"
|
||||
class="max-w-[500px] px-[8px]"
|
||||
size="mini"
|
||||
:step="1"
|
||||
:min="0"
|
||||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.waitTimeMs') }}:</div>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
export interface WaitTimeContentProps {
|
||||
id: string | number;
|
||||
waitTime: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: WaitTimeContentProps;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: WaitTimeContentProps): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerData = ref(props.data);
|
||||
|
||||
watchEffect(() => {
|
||||
innerData.value = props.data;
|
||||
});
|
||||
|
||||
function handleInputChange() {
|
||||
nextTick(() => {
|
||||
emit('change', innerData.value);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
:keyword="props.stepKeyword"
|
||||
:expand-all="props.expandAll"
|
||||
:node-more-actions="stepMoreActions"
|
||||
:filter-more-action-func="setStepMoreAction"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:virtual-list-props="{
|
||||
height: '100%',
|
||||
|
@ -18,7 +19,7 @@
|
|||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
}"
|
||||
title-class="step-tree-node-title"
|
||||
node-highlight-background-color="var(--color-text-n9)"
|
||||
node-highlight-class="step-tree-node-focus"
|
||||
action-on-node-click="expand"
|
||||
disabled-title-tooltip
|
||||
checkable
|
||||
|
@ -28,6 +29,7 @@
|
|||
@expand="handleStepExpand"
|
||||
@more-actions-close="() => setFocusNodeKey('')"
|
||||
@more-action-select="handleStepMoreActionSelect"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<template #title="step">
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
|
@ -41,7 +43,11 @@
|
|||
<!-- 步骤展开折叠按钮 -->
|
||||
<a-tooltip
|
||||
v-if="step.children?.length > 0"
|
||||
:content="t('apiScenario.expandStepTip', { count: step.children.length })"
|
||||
:content="
|
||||
t(step.expanded ? 'apiScenario.collapseStepTip' : 'apiScenario.expandStepTip', {
|
||||
count: step.children.length,
|
||||
})
|
||||
"
|
||||
>
|
||||
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
|
||||
<MsIcon
|
||||
|
@ -71,28 +77,33 @@
|
|||
<!-- 步骤整体内容 -->
|
||||
<div class="relative flex flex-1 items-center gap-[4px]">
|
||||
<!-- 步骤差异内容,按步骤类型展示不同组件 -->
|
||||
<component :is="getStepContent(step)" :data="getStepContentData(step)" />
|
||||
<component
|
||||
:is="getStepContent(step)"
|
||||
:data="step"
|
||||
@quick-input="setQuickInput(step, $event)"
|
||||
@change="handleStepContentChange($event, step)"
|
||||
@click.stop
|
||||
/>
|
||||
<!-- API、CASE、场景步骤名称 -->
|
||||
<template v-if="checkStepIsApi(step)">
|
||||
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
|
||||
<div
|
||||
v-if="step.id === showStepNameEditInputStepId"
|
||||
class="absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
class="name-warp 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)"
|
||||
@press-enter="applyStepNameChange(step)"
|
||||
@blur="applyStepNameChange(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)]">
|
||||
<div class="one-line-text mr-[4px] max-w-[150px] font-medium text-[var(--color-text-1)]">
|
||||
{{ step.name }}
|
||||
</div>
|
||||
<MsIcon
|
||||
|
@ -103,70 +114,57 @@
|
|||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<!-- 其他步骤描述 -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="step.id === showStepDescEditInputStepId"
|
||||
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="tempStepDesc"
|
||||
:default-value="step.description || t('apiScenario.pleaseInputStepDesc')"
|
||||
:placeholder="t('apiScenario.pleaseInputStepDesc')"
|
||||
:max-length="255"
|
||||
size="small"
|
||||
@press-enter="applyStepDescChange(step)"
|
||||
@blur="applyStepDescChange(step)"
|
||||
@click.stop
|
||||
>
|
||||
<template #prefix>
|
||||
{{ t('common.desc') }}
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<a-tooltip :content="step.description" :disabled="!step.description">
|
||||
<div class="step-name-container">
|
||||
<div
|
||||
:class="`one-line-text mr-[4px] ${
|
||||
step.type === ScenarioStepType.ONLY_ONCE_CONTROL ? 'max-w-[750px]' : 'max-w-[150px]'
|
||||
} font-normal text-[var(--color-text-4)]`"
|
||||
>
|
||||
{{ step.description || t('apiScenario.pleaseInputStepDesc') }}
|
||||
</div>
|
||||
<MsIcon
|
||||
type="icon-icon_edit_outlined"
|
||||
class="edit-script-name-icon"
|
||||
@click.stop="handleStepDescClick(step)"
|
||||
/>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra="step">
|
||||
<a-trigger
|
||||
trigger="click"
|
||||
class="arco-trigger-menu absolute"
|
||||
content-class="w-[160px]"
|
||||
position="br"
|
||||
@popup-visible-change="handleActionTriggerChange($event, step)"
|
||||
>
|
||||
<MsButton
|
||||
:id="step.id"
|
||||
type="icon"
|
||||
class="ms-tree-node-extra__btn !mr-[4px]"
|
||||
<stepInsertStepTrigger
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="steps"
|
||||
:step="step"
|
||||
@click="setFocusNodeKey(step.id)"
|
||||
>
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<actionDropdown
|
||||
v-model:visible="step.actionDropdownVisible"
|
||||
position="br"
|
||||
class="scenario-action-dropdown"
|
||||
:popup-translate="[-7, -10]"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType, step)"
|
||||
>
|
||||
<span></span>
|
||||
</actionDropdown>
|
||||
<div class="arco-trigger-menu-inner">
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'addChildStep' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'addChildStep')"
|
||||
>
|
||||
<icon-plus size="12" />
|
||||
{{ t('apiScenario.addChildStep') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'insertBefore' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'insertBefore')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertBefore') }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'arco-trigger-menu-item !mx-0 !w-full',
|
||||
activeAction === 'insertAfter' ? 'step-tree-active-action' : '',
|
||||
]"
|
||||
@click="handleTriggerActionClick(step, 'insertAfter')"
|
||||
>
|
||||
<icon-left size="12" />
|
||||
{{ t('apiScenario.insertAfter') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-trigger>
|
||||
@other-create="handleOtherCreate"
|
||||
@close="setFocusNodeKey('')"
|
||||
/>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<executeStatus v-if="step.status" :status="step.status" size="small" />
|
||||
|
@ -180,59 +178,88 @@
|
|||
</template>
|
||||
</MsTree>
|
||||
</a-spin>
|
||||
<actionDropdown
|
||||
class="scenario-action-dropdown"
|
||||
@select="(val) => handleActionSelect(val as ScenarioAddStepActionType)"
|
||||
>
|
||||
<createStepActions v-model:selected-keys="selectedKeys" v-model:steps="steps" @other-create="handleOtherCreate">
|
||||
<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" />
|
||||
</createStepActions>
|
||||
<!-- todo 执行、上传文件、转存文件等需要传入相关方法; 当前场景环境使用的是假数据; add-step暂时只是将数据传递到当前组件的customDemoStep对象中,用于再次打开的时候测试编辑功能 -->
|
||||
<customApiDrawer
|
||||
v-if="customApiDrawerVisible"
|
||||
v-model:visible="customApiDrawerVisible"
|
||||
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
|
||||
:request="customDemoStep"
|
||||
@add-step="addCustomApiStep"
|
||||
/>
|
||||
<scriptOperationDrawer v-model:visible="scriptOperationDrawerVisible" />
|
||||
<importApiDrawer v-if="importApiDrawerVisible" v-model:visible="importApiDrawerVisible" />
|
||||
<scriptOperationDrawer v-if="scriptOperationDrawerVisible" v-model:visible="scriptOperationDrawerVisible" />
|
||||
<a-modal
|
||||
v-model:visible="showQuickInput"
|
||||
:title="quickInputDataKey ? t(`apiScenario.${quickInputDataKey}`) : ''"
|
||||
:ok-text="t('apiTestDebug.apply')"
|
||||
:ok-button-props="{ disabled: !quickInputParamValue || quickInputParamValue.trim() === '' }"
|
||||
class="ms-modal-form"
|
||||
body-class="!p-0"
|
||||
:width="680"
|
||||
title-align="start"
|
||||
@ok="applyQuickInput"
|
||||
@close="clearQuickInput"
|
||||
>
|
||||
<MsCodeEditor
|
||||
v-if="showQuickInput"
|
||||
v-model:model-value="quickInputParamValue"
|
||||
theme="MS-text"
|
||||
height="300px"
|
||||
:show-full-screen="false"
|
||||
>
|
||||
<template #rightTitle>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-[var(--color-text-4)]">
|
||||
{{ t('apiTestDebug.quickInputParamsTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||
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 createStepActions from './createAction/createStepActions.vue';
|
||||
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.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 useAppStore from '@/store/modules/app';
|
||||
import { deleteNode, findNodeByKey, getGenerateId, insertNode, mapTree } from '@/utils';
|
||||
import { deleteNode, findNodeByKey, getGenerateId, handleTreeDragDrop, insertNode, mapTree, TreeNode } from '@/utils';
|
||||
|
||||
import { CustomApiStep } from '@/models/apiTest/scenario';
|
||||
import { CustomApiStep, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
|
||||
import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
import { defaultStepItemCommon } from '../config';
|
||||
// 非首屏渲染必要组件,异步加载
|
||||
const MsCodeEditor = defineAsyncComponent(() => import('@/components/pure/ms-code-editor/index.vue'));
|
||||
const customApiDrawer = defineAsyncComponent(() => import('../common/customApiDrawer.vue'));
|
||||
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
|
||||
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
|
||||
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
|
@ -253,6 +280,16 @@
|
|||
checked: boolean; // 是否选中
|
||||
expanded: boolean; // 是否展开
|
||||
actionDropdownVisible?: boolean; // 是否展示操作下拉
|
||||
parent?: ScenarioStepItem | ScenarioStepItem[]; // 父级节点,第一层的父级节点为undefined
|
||||
loopNum: number;
|
||||
loopType: 'num' | 'while' | 'forEach';
|
||||
loopSpace: number;
|
||||
variableName: string;
|
||||
variablePrefix: string;
|
||||
loopWhileType: ScenarioStepLoopWhileType;
|
||||
variableVal: string;
|
||||
condition: string;
|
||||
overTime: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -266,15 +303,18 @@
|
|||
const steps = defineModel<ScenarioStepItem[]>('steps', {
|
||||
required: true,
|
||||
});
|
||||
const checkedKeys = defineModel<string[]>('checkedKeys', {
|
||||
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const selectedKeys = ref<string[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const loading = ref(false);
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string>(''); // 聚焦的key
|
||||
|
||||
/**
|
||||
* 根据步骤类型获取步骤内容组件
|
||||
*/
|
||||
function getStepContent(step: ScenarioStepItem) {
|
||||
switch (step.type) {
|
||||
case ScenarioStepType.QUOTE_API:
|
||||
|
@ -287,41 +327,10 @@
|
|||
return loopControlContent;
|
||||
case ScenarioStepType.CONDITION_CONTROL:
|
||||
return conditionContent;
|
||||
case ScenarioStepType.ONLY_ONCE_CONTROL:
|
||||
return onlyOnceControlContent;
|
||||
case ScenarioStepType.WAIT_TIME:
|
||||
return waitTimeContent;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getStepContentData(step: ScenarioStepItem) {
|
||||
switch (step.type) {
|
||||
case ScenarioStepType.QUOTE_API:
|
||||
case ScenarioStepType.QUOTE_CASE:
|
||||
case ScenarioStepType.QUOTE_SCENARIO:
|
||||
return {
|
||||
id: step.id,
|
||||
belongProjectId: step.belongProjectId,
|
||||
belongProjectName: step.belongProjectName,
|
||||
num: step.num,
|
||||
name: step.name,
|
||||
type: step.type,
|
||||
currentProjectId: appStore.currentProjectId,
|
||||
};
|
||||
case ScenarioStepType.CUSTOM_API:
|
||||
return {};
|
||||
case ScenarioStepType.LOOP_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.CONDITION_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.ONLY_ONCE_CONTROL:
|
||||
return {};
|
||||
case ScenarioStepType.WAIT_TIME:
|
||||
return {};
|
||||
default:
|
||||
return '';
|
||||
return () => null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,25 +360,49 @@
|
|||
].includes(step.type);
|
||||
}
|
||||
|
||||
const activeAction = ref('');
|
||||
function handleTriggerActionClick(step: ScenarioStepItem, action: string) {
|
||||
step.actionDropdownVisible = true;
|
||||
activeAction.value = action;
|
||||
}
|
||||
|
||||
function handleActionTriggerChange(val: boolean, step: ScenarioStepItem) {
|
||||
if (!val) {
|
||||
activeAction.value = '';
|
||||
step.actionDropdownVisible = false;
|
||||
setFocusNodeKey('');
|
||||
/**
|
||||
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||
*/
|
||||
function isParentSelected(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
|
||||
if (parent && selectedKeys.value.includes(parent.id)) {
|
||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(step.id);
|
||||
}
|
||||
}
|
||||
|
||||
const stepMoreActions: ActionsItem[] = [
|
||||
{
|
||||
label: 'common.execute',
|
||||
eventTag: 'execute',
|
||||
label: 'common.copy',
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
label: 'common.delete',
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
function setStepMoreAction(items: ActionsItem[], node: MsTreeNodeData) {
|
||||
if ((node as ScenarioStepItem).type === ScenarioStepType.CUSTOM_API) {
|
||||
// 自定义请求
|
||||
return [
|
||||
{
|
||||
label: 'common.copy',
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
label: 'apiScenario.saveAsApi',
|
||||
eventTag: 'saveAsApi',
|
||||
},
|
||||
{
|
||||
label: 'common.delete',
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_SCENARIO) {
|
||||
return [
|
||||
{
|
||||
label: 'common.copy',
|
||||
eventTag: 'copy',
|
||||
|
@ -384,11 +417,29 @@
|
|||
danger: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_CASE) {
|
||||
return [
|
||||
{
|
||||
label: 'common.copy',
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
label: 'apiScenario.saveAsCase',
|
||||
eventTag: 'saveAsCase',
|
||||
},
|
||||
{
|
||||
label: 'common.delete',
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return stepMoreActions;
|
||||
}
|
||||
|
||||
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||
switch (item.eventTag) {
|
||||
case 'execute':
|
||||
console.log('执行步骤', node);
|
||||
break;
|
||||
case 'copy':
|
||||
const id = getGenerateId();
|
||||
insertNode<ScenarioStepItem>(
|
||||
|
@ -397,27 +448,20 @@
|
|||
{
|
||||
...cloneDeep(
|
||||
mapTree<ScenarioStepItem>(node, (childNode) => {
|
||||
const childId = getGenerateId();
|
||||
if (selectedKeys.value.includes(node.id)) {
|
||||
// 复制一个已选中的节点,需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(childId);
|
||||
}
|
||||
return {
|
||||
...childNode,
|
||||
id: childId,
|
||||
id: getGenerateId(), // TODO:引用类型额外需要一个复制来源 ID
|
||||
};
|
||||
})[0]
|
||||
),
|
||||
name: `copy-${node.name}`,
|
||||
order: node.order + 1,
|
||||
id,
|
||||
},
|
||||
'after',
|
||||
isParentSelected,
|
||||
'id'
|
||||
);
|
||||
if (selectedKeys.value.includes(node.id)) {
|
||||
// 复制一个已选中的节点,需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(id);
|
||||
}
|
||||
break;
|
||||
case 'config':
|
||||
console.log('config', node);
|
||||
|
@ -443,12 +487,13 @@
|
|||
tempStepName.value = step.name;
|
||||
showStepNameEditInputStepId.value = step.id;
|
||||
nextTick(() => {
|
||||
const input = treeRef.value?.$el.querySelector('.arco-input') as HTMLInputElement;
|
||||
// 等待输入框渲染完成后聚焦
|
||||
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||
input?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function applyStepChange(step: ScenarioStepItem) {
|
||||
function applyStepNameChange(step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
if (realStep) {
|
||||
realStep.name = tempStepName.value;
|
||||
|
@ -456,6 +501,38 @@
|
|||
showStepNameEditInputStepId.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理步骤名称编辑
|
||||
*/
|
||||
const showStepDescEditInputStepId = ref<string | number>('');
|
||||
const tempStepDesc = ref('');
|
||||
function handleStepDescClick(step: ScenarioStepItem) {
|
||||
tempStepDesc.value = step.description;
|
||||
showStepDescEditInputStepId.value = step.id;
|
||||
nextTick(() => {
|
||||
// 等待输入框渲染完成后聚焦
|
||||
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||
input?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function applyStepDescChange(step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
if (realStep) {
|
||||
realStep.description = tempStepDesc.value;
|
||||
}
|
||||
showStepDescEditInputStepId.value = '';
|
||||
}
|
||||
|
||||
function handleStepContentChange($event, step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
if (realStep) {
|
||||
Object.keys($event).forEach((key) => {
|
||||
realStep[key] = $event[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理步骤展开折叠
|
||||
*/
|
||||
|
@ -482,63 +559,70 @@
|
|||
const importApiDrawerVisible = ref(false);
|
||||
const customApiDrawerVisible = ref(false);
|
||||
const scriptOperationDrawerVisible = ref(false);
|
||||
const activeStep = ref<ScenarioStepItem>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
||||
|
||||
function handleActionSelect(val: ScenarioAddStepActionType, step?: ScenarioStepItem) {
|
||||
switch (val) {
|
||||
function handleOtherCreate(
|
||||
type:
|
||||
| ScenarioAddStepActionType.IMPORT_SYSTEM_API
|
||||
| ScenarioAddStepActionType.CUSTOM_API
|
||||
| ScenarioAddStepActionType.SCRIPT_OPERATION,
|
||||
step?: ScenarioStepItem
|
||||
) {
|
||||
activeStep.value = step;
|
||||
switch (type) {
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
importApiDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
customApiDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.LOOP_CONTROL:
|
||||
steps.value.push({
|
||||
...defaultStepItemCommon,
|
||||
id: Date.now(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.LOOP_CONTROL,
|
||||
name: '循环控制',
|
||||
description: '循环控制描述',
|
||||
});
|
||||
break;
|
||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||
steps.value.push({
|
||||
...defaultStepItemCommon,
|
||||
id: Date.now(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.CONDITION_CONTROL,
|
||||
name: '条件控制',
|
||||
description: '条件控制描述',
|
||||
});
|
||||
break;
|
||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||
steps.value.push({
|
||||
...defaultStepItemCommon,
|
||||
id: Date.now(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
name: '仅执行一次',
|
||||
description: '仅执行一次描述',
|
||||
});
|
||||
break;
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
scriptOperationDrawerVisible.value = true;
|
||||
break;
|
||||
case ScenarioAddStepActionType.WAIT_TIME:
|
||||
steps.value.push({
|
||||
...defaultStepItemCommon,
|
||||
id: Date.now(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.WAIT_TIME,
|
||||
name: '等待时间',
|
||||
description: '等待时间描述',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (step) {
|
||||
document.getElementById(step.id.toString())?.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件夹树节点拖拽事件
|
||||
* @param tree 树数据
|
||||
* @param dragNode 拖拽节点
|
||||
* @param dropNode 释放节点
|
||||
* @param dropPosition 释放位置(取值:-1,,0,,1。 -1:dropNodeId节点之前。 0:dropNodeId节点内。 1:dropNodeId节点后)
|
||||
*/
|
||||
function handleDrop(
|
||||
tree: MsTreeNodeData[],
|
||||
dragNode: MsTreeNodeData,
|
||||
dropNode: MsTreeNodeData,
|
||||
dropPosition: number
|
||||
) {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (dropPosition === 0) {
|
||||
// 拖拽到节点内
|
||||
if (selectedKeys.value.includes(dropNode.id)) {
|
||||
// 释放位置的节点已选中,则需要把拖动的节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(dragNode.id);
|
||||
}
|
||||
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
|
||||
// 释放位置的节点的父节点已选中,则需要把拖动的节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(dragNode.id);
|
||||
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
|
||||
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点从选中的节点中移除
|
||||
selectedKeys.value = selectedKeys.value.filter((e) => e !== dragNode.id);
|
||||
}
|
||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
|
||||
if (dragResult) {
|
||||
Message.success(t('common.moveSuccess'));
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
nextTick(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -549,6 +633,49 @@
|
|||
customDemoStep.value = { ...step };
|
||||
}
|
||||
|
||||
const showQuickInput = ref(false);
|
||||
const quickInputParamValue = ref('');
|
||||
const quickInputDataKey = ref('');
|
||||
|
||||
function setQuickInput(step: ScenarioStepItem, dataKey: string) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
if (realStep) {
|
||||
activeStep.value = realStep as ScenarioStepItem;
|
||||
}
|
||||
quickInputDataKey.value = dataKey;
|
||||
quickInputParamValue.value = step.variableVal;
|
||||
showQuickInput.value = true;
|
||||
}
|
||||
|
||||
function clearQuickInput() {
|
||||
activeStep.value = undefined;
|
||||
quickInputParamValue.value = '';
|
||||
quickInputDataKey.value = '';
|
||||
}
|
||||
|
||||
function applyQuickInput() {
|
||||
if (activeStep.value) {
|
||||
activeStep.value[quickInputDataKey.value] = quickInputParamValue.value;
|
||||
showQuickInput.value = false;
|
||||
clearQuickInput();
|
||||
}
|
||||
}
|
||||
|
||||
const dbClick = ref({
|
||||
e: null as MouseEvent | null,
|
||||
timeStamp: 0,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
useEventListener(treeRef.value?.$el, 'dblclick', (e) => {
|
||||
dbClick.value.e = e;
|
||||
dbClick.value.timeStamp = Date.now();
|
||||
});
|
||||
});
|
||||
|
||||
// 向子孙组件提供属性
|
||||
provide('dbClick', readonly(dbClick));
|
||||
|
||||
defineExpose({
|
||||
checkAll,
|
||||
});
|
||||
|
@ -585,6 +712,7 @@
|
|||
.loop-levels(0, 99); // 最大层级
|
||||
:deep(.arco-tree-node) {
|
||||
padding: 0 8px;
|
||||
min-width: 1000px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-medium) !important;
|
||||
&:not(:first-child) {
|
||||
|
@ -597,7 +725,7 @@
|
|||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
@apply !cursor-pointer;
|
||||
@apply !cursor-pointer bg-white;
|
||||
|
||||
padding: 12px 4px;
|
||||
&:hover {
|
||||
|
@ -624,11 +752,6 @@
|
|||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
&[draggable='true']:hover {
|
||||
.step-node-content {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title-text {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
@ -640,19 +763,28 @@
|
|||
@apply hidden;
|
||||
}
|
||||
.arco-tree-node-drag-icon {
|
||||
@apply ml-0;
|
||||
|
||||
top: 13px;
|
||||
left: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
.arco-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
@apply hidden;
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
gap: 4px;
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
}
|
||||
:deep(.arco-tree-node-selected) {
|
||||
.arco-tree-node-title {
|
||||
.step-tree-node-title {
|
||||
font-weight: 400;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.step-tree-node-focus) {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
.arco-tree-node-title {
|
||||
background-color: var(--color-text-n9) !important;
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply !visible !w-auto;
|
||||
}
|
||||
}
|
||||
</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" is-new />
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:step="scenario.stepInfo" 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" />
|
||||
|
@ -124,10 +124,9 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { Scenario } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ApiScenarioStatus, RequestCaseStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
import type { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
|
||||
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
// 组成部分异步导入
|
||||
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
||||
|
@ -143,13 +142,8 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const activeKey = ref<ScenarioCreateComposition>(ScenarioCreateComposition.STEP);
|
||||
const scenario = ref<any>({
|
||||
name: '',
|
||||
moduleId: 'root',
|
||||
stepInfo: {} as ScenarioStepInfo,
|
||||
status: RequestCaseStatus.PROCESSING,
|
||||
tags: [],
|
||||
params: [],
|
||||
const scenario = defineModel<Scenario>('scenario', {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
BASE_INFO
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]">
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" />
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" :step="previewDetail.step" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioCreateComposition.PARAMS"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<MsCard no-content-padding simple>
|
||||
<div class="p-[24px_24px_8px_24px]">
|
||||
<div class="flex items-center justify-between p-[24px_24px_8px_24px]">
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeApiTab"
|
||||
v-model:active-tab="activeScenarioTab"
|
||||
v-model:tabs="apiTabs"
|
||||
class="flex-1 overflow-hidden"
|
||||
@add="newTab"
|
||||
|
@ -15,9 +15,14 @@
|
|||
</a-tooltip>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider class="!my-0" />
|
||||
<div v-if="activeApiTab.id === 'all'" class="pageWrap">
|
||||
<div v-if="activeScenarioTab.id === 'all'" class="pageWrap">
|
||||
<MsSplitBox :size="300" :max="0.5">
|
||||
<template #first>
|
||||
<div class="flex h-full flex-col">
|
||||
|
@ -52,11 +57,11 @@
|
|||
</template>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
<div v-else-if="activeApiTab.is" class="pageWrap">
|
||||
<detail :detail="activeApiTab"></detail>
|
||||
<div v-else-if="activeScenarioTab.isNew" class="pageWrap">
|
||||
<create v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
|
||||
</div>
|
||||
<div v-else class="pageWrap">
|
||||
<create :module-tree="folderTree"></create>
|
||||
<detail :detail="activeScenarioTab"></detail>
|
||||
</div>
|
||||
</MsCard>
|
||||
</template>
|
||||
|
@ -66,49 +71,63 @@
|
|||
* @description 接口测试-接口场景主页
|
||||
*/
|
||||
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import scenarioModuleTree from './components/scenarioModuleTree.vue';
|
||||
import { ScenarioStepInfo } from './components/step/index.vue';
|
||||
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
|
||||
|
||||
import { getTrashModuleCount } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import router from '@/router';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ApiScenarioGetModuleParams } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioGetModuleParams, Scenario } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { RequestDefinitionStatus } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import useAppStore from '../../../store/modules/app';
|
||||
|
||||
// 异步导入
|
||||
const detail = defineAsyncComponent(() => import('./detail/index.vue'));
|
||||
const create = defineAsyncComponent(() => import('./create/index.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const apiTabs = ref<TabItem[]>([
|
||||
const apiTabs = ref<Scenario[]>([
|
||||
{
|
||||
id: 'all',
|
||||
label: t('apiScenario.allScenario'),
|
||||
closable: false,
|
||||
},
|
||||
} as Scenario,
|
||||
]);
|
||||
const activeApiTab = ref<TabItem>(apiTabs.value[0]);
|
||||
const activeScenarioTab = ref<Scenario>(apiTabs.value[0]);
|
||||
|
||||
function newTab() {
|
||||
apiTabs.value.push({
|
||||
id: `newTab${apiTabs.value.length}`,
|
||||
label: `New Tab ${apiTabs.value.length}`,
|
||||
id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
|
||||
label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
|
||||
closable: true,
|
||||
isNew: true,
|
||||
name: '',
|
||||
moduleId: 'root',
|
||||
stepInfo: {
|
||||
id: new Date().getTime(),
|
||||
steps: [],
|
||||
executeTime: '',
|
||||
executeSuccessCount: 0,
|
||||
executeFailCount: 0,
|
||||
} as ScenarioStepInfo,
|
||||
status: RequestDefinitionStatus.PROCESSING,
|
||||
tags: [],
|
||||
params: [],
|
||||
executeLoading: false,
|
||||
unSaved: false,
|
||||
});
|
||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
}
|
||||
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
|
@ -153,6 +172,21 @@
|
|||
});
|
||||
recycleModulesCount.value = res.all;
|
||||
});
|
||||
|
||||
const saveLoading = ref(false);
|
||||
|
||||
async function saveScenario() {
|
||||
saveLoading.value = true;
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve('');
|
||||
}, 1000);
|
||||
});
|
||||
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess'));
|
||||
activeScenarioTab.value.isNew = false;
|
||||
activeScenarioTab.value.unSaved = false;
|
||||
saveLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -92,7 +92,7 @@ export default {
|
|||
'apiScenario.executeTime': '执行时间:',
|
||||
'apiScenario.executeResult': '执行结果',
|
||||
'apiScenario.checkReport': '查看报告',
|
||||
'apiScenario.searchByName': '通过名称搜索',
|
||||
'apiScenario.searchByName': '通过步骤名称/描述搜索',
|
||||
'apiScenario.api': '接口',
|
||||
'apiScenario.case': '用例',
|
||||
'apiScenario.scenario': '场景',
|
||||
|
@ -104,9 +104,34 @@ export default {
|
|||
'apiScenario.detailName': '名称',
|
||||
'apiScenario.crossProject': '跨项目',
|
||||
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
|
||||
'apiScenario.collapseStepTip': '折叠 {count} 个子步骤',
|
||||
'apiScenario.addChildStep': '添加子步骤',
|
||||
'apiScenario.insertBefore': '在之前插入步骤',
|
||||
'apiScenario.insertAfter': '在之后插入步骤',
|
||||
'apiScenario.num': '次数',
|
||||
'apiScenario.space': '间隔(ms)',
|
||||
'apiScenario.overTime': '超时(ms)',
|
||||
'apiScenario.waitTimeMs': '等待(ms)',
|
||||
'apiScenario.pleaseInputStepDesc': '请输入步骤描述',
|
||||
'apiScenario.variableName': '变量名称{suffix}',
|
||||
'apiScenario.variablePrefix': '变量前缀',
|
||||
'apiScenario.variableVal': '变量值',
|
||||
'apiScenario.condition': '条件',
|
||||
'apiScenario.expression': '表达式',
|
||||
'apiScenario.equal': '等于',
|
||||
'apiScenario.notEqualTo': '不等于',
|
||||
'apiScenario.greater': '大于',
|
||||
'apiScenario.less': '小于',
|
||||
'apiScenario.greaterOrEqual': '大于等于',
|
||||
'apiScenario.lessOrEqual': '小于等于',
|
||||
'apiScenario.include': '包含',
|
||||
'apiScenario.notInclude': '不包含',
|
||||
'apiScenario.null': '空',
|
||||
'apiScenario.notNull': '非空',
|
||||
'apiScenario.range': '范围',
|
||||
'apiScenario.topStep': '一级步骤',
|
||||
'apiScenario.allStep': '所有子步骤',
|
||||
'apiScenario.saveAsApi': '保存为新接口',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ApiScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioStatus } from '@/enums/apiEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
|
@ -331,7 +331,7 @@
|
|||
});
|
||||
|
||||
// 恢复
|
||||
async function recover(record?: ApiScenarioDetail, isBatch?: boolean) {
|
||||
async function recover(record?: ApiScenarioTableItem, isBatch?: boolean) {
|
||||
try {
|
||||
if (isBatch) {
|
||||
recoverLoading.value = true;
|
||||
|
@ -360,7 +360,7 @@
|
|||
/**
|
||||
* 删除接口
|
||||
*/
|
||||
function deleteOperation(record?: ApiScenarioDetail, isBatch?: boolean, params?: BatchActionQueryParams) {
|
||||
function deleteOperation(record?: ApiScenarioTableItem, isBatch?: boolean, params?: BatchActionQueryParams) {
|
||||
let title = t('api_scenario.table.deleteScenarioTipTitle', { name: record?.name });
|
||||
let selectIds = [record?.id || ''];
|
||||
if (isBatch) {
|
||||
|
|
Loading…
Reference in New Issue