feat(接口场景): 场景步骤 80%
This commit is contained in:
parent
ca99eeca14
commit
e2bef32de2
|
@ -38,7 +38,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@7polo/kity": "2.0.8",
|
"@7polo/kity": "2.0.8",
|
||||||
"@7polo/kityminder-core": "1.4.53",
|
"@7polo/kityminder-core": "1.4.53",
|
||||||
"@arco-design/web-vue": "^2.54.4",
|
"@arco-design/web-vue": "^2.55.0",
|
||||||
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
||||||
"@form-create/arco-design": "^3.1.23",
|
"@form-create/arco-design": "^3.1.23",
|
||||||
"@halo-dev/richtext-editor": "0.0.0-alpha.33",
|
"@halo-dev/richtext-editor": "0.0.0-alpha.33",
|
||||||
|
|
|
@ -137,23 +137,18 @@
|
||||||
width: 960px;
|
width: 960px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-modal-response {
|
.ms-modal-response {
|
||||||
.arco-modal {
|
.arco-modal {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: 523px
|
height: 523px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ms-modal-response-body {
|
||||||
.ms-modal-response-body{
|
.arco-modal-body {
|
||||||
.arco-modal-body{
|
|
||||||
padding: 0;
|
|
||||||
overflow: inherit;
|
overflow: inherit;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.ms-modal-small {
|
.ms-modal-small {
|
||||||
.arco-modal {
|
.arco-modal {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
|
@ -394,9 +389,8 @@
|
||||||
}
|
}
|
||||||
.arco-checkbox-icon-check {
|
.arco-checkbox-icon-check {
|
||||||
@apply text-white;
|
@apply text-white;
|
||||||
.arco-checkbox-icon {
|
|
||||||
background-color: rgb(var(--primary-5));
|
background-color: rgb(var(--primary-5));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.arco-checkbox {
|
.arco-checkbox {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<MsUpload
|
<MsUpload
|
||||||
v-model:file-list="innerFileList"
|
v-model:file-list="fileList"
|
||||||
accept="none"
|
accept="none"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
|
@ -229,7 +229,7 @@
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerFileList = defineModel<MsFileItem[]>('fileList', {
|
const fileList = defineModel<MsFileItem[]>('fileList', {
|
||||||
// TODO:这里的文件含有组件内部定义的属性,应该继承MsFileItem类型并扩展声明组件定义的类型属性
|
// TODO:这里的文件含有组件内部定义的属性,应该继承MsFileItem类型并扩展声明组件定义的类型属性
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
@ -245,7 +245,7 @@
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
// 回显文件
|
// 回显文件
|
||||||
const defaultFiles = innerFileList.value.filter((item) => item) || [];
|
const defaultFiles = fileList.value.filter((item) => item) || [];
|
||||||
if (defaultFiles.length > 0) {
|
if (defaultFiles.length > 0) {
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
inputFiles.value = defaultFiles.map((item) => ({
|
inputFiles.value = defaultFiles.map((item) => ({
|
||||||
|
@ -266,18 +266,18 @@
|
||||||
|
|
||||||
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
|
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
innerFileList.value.push(fileItem);
|
fileList.value.push(fileItem);
|
||||||
inputFiles.value.push({
|
inputFiles.value.push({
|
||||||
...fileItem,
|
...fileItem,
|
||||||
value: fileItem[props.fields.id] || fileItem.uid || '',
|
value: fileItem[props.fields.id] || fileItem.uid || '',
|
||||||
label: fileItem[props.fields.name] || fileItem.name || '',
|
label: fileItem[props.fields.name] || fileItem.name || '',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
innerFileList.value = [fileItem];
|
fileList.value = [fileItem];
|
||||||
inputFileName.value = fileItem.name || '';
|
inputFileName.value = fileItem.name || '';
|
||||||
}
|
}
|
||||||
fileItem.local = true;
|
fileItem.local = true;
|
||||||
emit('change', innerFileList.value, fileItem);
|
emit('change', fileList.value, fileItem);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 在 emit 文件上去之后再关闭菜单
|
// 在 emit 文件上去之后再关闭菜单
|
||||||
buttonDropDownVisible.value = false;
|
buttonDropDownVisible.value = false;
|
||||||
|
@ -295,7 +295,7 @@
|
||||||
|
|
||||||
// 监视文件列表处理关联和本地文件
|
// 监视文件列表处理关联和本地文件
|
||||||
watch(
|
watch(
|
||||||
() => innerFileList.value,
|
() => fileList.value,
|
||||||
(arr) => {
|
(arr) => {
|
||||||
getListFunParams.value.combine.hiddenIds = arr
|
getListFunParams.value.combine.hiddenIds = arr
|
||||||
.filter((item) => !item.local)
|
.filter((item) => !item.local)
|
||||||
|
@ -308,9 +308,9 @@
|
||||||
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
|
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
|
||||||
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
|
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
|
||||||
if (props.mode === 'button') {
|
if (props.mode === 'button') {
|
||||||
innerFileList.value.push(...fileResultList);
|
fileList.value.push(...fileResultList);
|
||||||
} else if (props.multiple) {
|
} else if (props.multiple) {
|
||||||
innerFileList.value.push(...fileResultList);
|
fileList.value.push(...fileResultList);
|
||||||
inputFiles.value.push(
|
inputFiles.value.push(
|
||||||
...fileResultList.map((item) => ({
|
...fileResultList.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
@ -321,10 +321,10 @@
|
||||||
} else {
|
} else {
|
||||||
// 单选文件
|
// 单选文件
|
||||||
const file = fileResultList[0];
|
const file = fileResultList[0];
|
||||||
innerFileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
|
fileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
|
||||||
inputFileName.value = file.name || '';
|
inputFileName.value = file.name || '';
|
||||||
}
|
}
|
||||||
emit('change', innerFileList.value);
|
emit('change', fileList.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputFilesPopoverVisible = ref(false);
|
const inputFilesPopoverVisible = ref(false);
|
||||||
|
@ -341,9 +341,7 @@
|
||||||
|
|
||||||
function handleClose(data: TagData) {
|
function handleClose(data: TagData) {
|
||||||
inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value);
|
inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value);
|
||||||
innerFileList.value = innerFileList.value.filter(
|
fileList.value = fileList.value.filter((item) => (item.uid || item[props.fields.id]) !== data.value);
|
||||||
(item) => (item[props.fields.id] || item.uid) !== (data[props.fields.id] || data.value)
|
|
||||||
);
|
|
||||||
if (inputFiles.value.length === 0) {
|
if (inputFiles.value.length === 0) {
|
||||||
inputFilesPopoverVisible.value = false;
|
inputFilesPopoverVisible.value = false;
|
||||||
}
|
}
|
||||||
|
@ -353,7 +351,7 @@
|
||||||
function handleFileClear() {
|
function handleFileClear() {
|
||||||
inputFileName.value = '';
|
inputFileName.value = '';
|
||||||
inputFiles.value = [];
|
inputFiles.value = [];
|
||||||
innerFileList.value = [];
|
fileList.value = [];
|
||||||
emit('change', []);
|
emit('change', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +365,7 @@
|
||||||
function handleOpenSaveAs(item: TagData) {
|
function handleOpenSaveAs(item: TagData) {
|
||||||
inputFilesPopoverVisible.value = false;
|
inputFilesPopoverVisible.value = false;
|
||||||
// 这里先判定 uid 是否存在,存在则是刚上传的文件;否则是已保存过后的详情文件
|
// 这里先判定 uid 是否存在,存在则是刚上传的文件;否则是已保存过后的详情文件
|
||||||
savingFile.value = innerFileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
|
savingFile.value = fileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
|
||||||
saveFilePopoverVisible.value = true;
|
saveFilePopoverVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -300,7 +300,7 @@
|
||||||
.ms-drawer-body-scrollbar {
|
.ms-drawer-body-scrollbar {
|
||||||
@apply h-full w-full overflow-auto;
|
@apply h-full w-full overflow-auto;
|
||||||
|
|
||||||
min-width: 680px;
|
min-width: 650px;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
.ms-drawer-body {
|
.ms-drawer-body {
|
||||||
|
|
|
@ -16,7 +16,8 @@ export default function useOpenNewPage() {
|
||||||
window.open(
|
window.open(
|
||||||
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
|
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
|
||||||
appStore.currentProjectId
|
appStore.currentProjectId
|
||||||
}&${queryParams}`
|
}&${queryParams}`,
|
||||||
|
'_blank'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||||
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
|
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
|
||||||
|
|
||||||
import { ApiDefinitionCustomField } from '@/models/apiTest/management';
|
import { ApiDefinitionCustomField } from '@/models/apiTest/management';
|
||||||
|
@ -184,13 +185,14 @@ export type ScenarioStepLoopType = 'num' | 'while' | 'forEach';
|
||||||
// 场景步骤-循环控制器-循环类型
|
// 场景步骤-循环控制器-循环类型
|
||||||
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
|
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
|
||||||
// 场景步骤-步骤插入类型
|
// 场景步骤-步骤插入类型
|
||||||
export type CreateStepAction = 'addChildStep' | 'insertBefore' | 'insertAfter' | undefined;
|
export type CreateStepAction = 'inside' | 'before' | 'after';
|
||||||
// 场景步骤
|
// 场景步骤
|
||||||
export interface Scenario {
|
export interface Scenario {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
moduleId: string | number;
|
moduleId: string | number;
|
||||||
stepInfo: ScenarioStepInfo;
|
stepInfo: ScenarioStepInfo;
|
||||||
|
priority: CaseLevel;
|
||||||
status: RequestDefinitionStatus;
|
status: RequestDefinitionStatus;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
params: Record<string, any>[];
|
params: Record<string, any>[];
|
||||||
|
|
|
@ -357,58 +357,70 @@ export function findNodePathByKey<T>(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在某个节点前/后插入新节点
|
* 在某个节点前/后插入单个新节点
|
||||||
* @param treeArr 目标树
|
* @param treeArr 目标树
|
||||||
* @param targetKey 目标节点唯一值
|
* @param targetKey 目标节点唯一值
|
||||||
* @param newNode 新节点
|
* @param newNodes 新节点树/数组
|
||||||
* @param position 插入位置
|
* @param position 插入位置
|
||||||
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
||||||
*/
|
*/
|
||||||
export function insertNode<T>(
|
export function insertNodes<T>(
|
||||||
treeArr: TreeNode<T>[],
|
treeArr: TreeNode<T>[],
|
||||||
targetKey: string | number,
|
targetKey: string | number,
|
||||||
newNode: TreeNode<T>,
|
newNodes: TreeNode<T> | TreeNode<T>[],
|
||||||
position: 'before' | 'after' | 'inside',
|
position: 'before' | 'after' | 'inside',
|
||||||
customFunc?: (node: TreeNode<T>, parent?: TreeNode<T>) => void,
|
customFunc?: (node: TreeNode<T>, parent?: TreeNode<T>) => void,
|
||||||
customKey = 'key'
|
customKey = 'key'
|
||||||
): void {
|
): void {
|
||||||
|
function insertNewNodes(
|
||||||
|
array: TreeNode<T>[],
|
||||||
|
startIndex: number,
|
||||||
|
parent: TreeNode<T> | undefined,
|
||||||
|
startOrder: number
|
||||||
|
) {
|
||||||
|
if (Array.isArray(newNodes)) {
|
||||||
|
// 插入节点数组
|
||||||
|
newNodes.forEach((newNode, index) => {
|
||||||
|
newNode.parent = parent;
|
||||||
|
newNode.order = startOrder + index;
|
||||||
|
});
|
||||||
|
array.splice(startIndex, 0, ...newNodes);
|
||||||
|
} else {
|
||||||
|
// 插入单个节点
|
||||||
|
newNodes.parent = parent;
|
||||||
|
newNodes.order = startOrder;
|
||||||
|
array.splice(startIndex, 0, newNodes);
|
||||||
|
}
|
||||||
|
// 更新插入节点之后的节点的 order
|
||||||
|
const newLength = Array.isArray(newNodes) ? newNodes.length : 1;
|
||||||
|
for (let j = startIndex + newLength; j < array.length; j++) {
|
||||||
|
array[j].order += newLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function insertNodeInTree(tree: TreeNode<T>[], parent?: TreeNode<T>): boolean {
|
function insertNodeInTree(tree: TreeNode<T>[], parent?: TreeNode<T>): boolean {
|
||||||
for (let i = 0; i < tree.length; i++) {
|
for (let i = 0; i < tree.length; i++) {
|
||||||
const node = tree[i];
|
const node = tree[i];
|
||||||
if (node[customKey] === targetKey) {
|
if (node[customKey] === targetKey) {
|
||||||
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后/内部插入新节点
|
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后/内部插入新节点
|
||||||
const childrenArray = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
|
const parentChildren = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
|
||||||
const index = childrenArray.findIndex((item) => item[customKey] === node[customKey]);
|
const index = parentChildren.findIndex((item) => item[customKey] === node[customKey]);
|
||||||
if (position === 'before') {
|
if (position === 'before') {
|
||||||
newNode.parent = parent || node.parent;
|
insertNewNodes(parentChildren, index, parent || node.parent, node.order);
|
||||||
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') {
|
} else if (position === 'after') {
|
||||||
newNode.parent = parent || node.parent;
|
insertNewNodes(parentChildren, index + 1, parent || node.parent, node.order + 1);
|
||||||
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') {
|
} else if (position === 'inside') {
|
||||||
if (!node.children) {
|
if (!node.children) {
|
||||||
node.children = [];
|
node.children = [];
|
||||||
}
|
}
|
||||||
newNode.parent = node;
|
insertNewNodes(node.children, node.children.length, node, node.children.length + 1);
|
||||||
newNode.order = node.children.length + 1;
|
|
||||||
node.children.push(newNode);
|
|
||||||
}
|
}
|
||||||
if (typeof customFunc === 'function') {
|
if (typeof customFunc === 'function') {
|
||||||
customFunc(newNode, parent);
|
if (Array.isArray(newNodes)) {
|
||||||
|
newNodes.forEach((newNode) => customFunc(newNode, parent || node.parent));
|
||||||
|
} else {
|
||||||
|
customFunc(newNodes, parent || node.parent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 插入后返回 true
|
// 插入后返回 true
|
||||||
return true;
|
return true;
|
||||||
|
@ -456,10 +468,10 @@ export function handleTreeDragDrop<T>(
|
||||||
|
|
||||||
// 拖动节点插入到目标节点的 children 数组中
|
// 拖动节点插入到目标节点的 children 数组中
|
||||||
if (dropPosition === 0) {
|
if (dropPosition === 0) {
|
||||||
insertNode(dropNode.parent?.children || treeArr, dropNode[customKey], dragNode, 'inside', undefined, customKey);
|
insertNodes(dropNode.parent?.children || treeArr, dropNode[customKey], dragNode, 'inside', undefined, customKey);
|
||||||
} else {
|
} else {
|
||||||
// 拖动节点插入到目标节点的前/后
|
// 拖动节点插入到目标节点的前/后
|
||||||
insertNode(
|
insertNodes(
|
||||||
dropNode.parent?.children || treeArr,
|
dropNode.parent?.children || treeArr,
|
||||||
dropNode[customKey],
|
dropNode[customKey],
|
||||||
dragNode,
|
dragNode,
|
||||||
|
|
|
@ -144,26 +144,28 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleFileChange(files: MsFileItem[]) {
|
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
|
||||||
if (!props.uploadTempFileApi) return;
|
if (!props.uploadTempFileApi) return;
|
||||||
if (files.length === 0) {
|
if (files.length === 0 && file === undefined) {
|
||||||
innerParams.value.binaryBody.file = undefined;
|
innerParams.value.binaryBody.file = undefined;
|
||||||
emit('change');
|
emit('change');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (fileList.value[0]?.local && fileList.value[0].file) {
|
if (file?.local && file.file) {
|
||||||
|
// 本地上传
|
||||||
appStore.showLoading();
|
appStore.showLoading();
|
||||||
const res = await props.uploadTempFileApi(fileList.value[0].file);
|
const res = await props.uploadTempFileApi(file.file);
|
||||||
innerParams.value.binaryBody.file = {
|
innerParams.value.binaryBody.file = {
|
||||||
...fileList.value[0],
|
...file,
|
||||||
fileId: res.data,
|
fileId: res.data,
|
||||||
fileName: fileList.value[0]?.name || '',
|
fileName: file?.name || '',
|
||||||
fileAlias: fileList.value[0]?.name || '',
|
fileAlias: file?.name || '',
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
appStore.hideLoading();
|
appStore.hideLoading();
|
||||||
} else {
|
} else {
|
||||||
|
// 关联文件
|
||||||
innerParams.value.binaryBody.file = {
|
innerParams.value.binaryBody.file = {
|
||||||
...fileList.value[0],
|
...fileList.value[0],
|
||||||
fileId: fileList.value[0]?.uid,
|
fileId: fileList.value[0]?.uid,
|
||||||
|
|
|
@ -178,7 +178,7 @@
|
||||||
function getResponsePreContent(type: keyof typeof ResponseComposition) {
|
function getResponsePreContent(type: keyof typeof ResponseComposition) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ResponseComposition.HEADER:
|
case ResponseComposition.HEADER:
|
||||||
return props.requestResult?.headers.trim();
|
return props.requestResult?.responseResult?.headers.trim();
|
||||||
case ResponseComposition.REAL_REQUEST:
|
case ResponseComposition.REAL_REQUEST:
|
||||||
return props.requestResult?.body
|
return props.requestResult?.body
|
||||||
? `${t('apiTestDebug.requestUrl')}:\n${props.requestResult.url}\n${t('apiTestDebug.header')}:\n${
|
? `${t('apiTestDebug.requestUrl')}:\n${props.requestResult.url}\n${t('apiTestDebug.header')}:\n${
|
||||||
|
|
|
@ -7,22 +7,22 @@
|
||||||
disabled-width-drag
|
disabled-width-drag
|
||||||
>
|
>
|
||||||
<div class="h-full w-full overflow-hidden">
|
<div class="h-full w-full overflow-hidden">
|
||||||
<a-tabs v-model:active-key="activeKey" @change="resetModuleAndTable">
|
<a-tabs v-model:active-key="activeKey" @change="resetModule">
|
||||||
<a-tab-pane key="api" :title="t('apiScenario.api')" />
|
<a-tab-pane key="api" :title="t('apiScenario.api')" />
|
||||||
<a-tab-pane key="case" :title="t('apiScenario.case')" />
|
<a-tab-pane key="case" :title="t('apiScenario.case')" />
|
||||||
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
|
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<a-divider :margin="0"></a-divider>
|
<a-divider :margin="0"></a-divider>
|
||||||
<div class="flex">
|
<div class="flex h-[calc(100%-49px)]">
|
||||||
<div class="w-[300px] border-r p-[16px]">
|
<div class="w-[300px] border-r p-[16px]">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="mb-[12px] flex items-center gap-[8px]">
|
<div class="mb-[12px] flex items-center gap-[8px]">
|
||||||
<MsProjectSelect v-model:project="currentProject" @change="resetModuleAndTable" />
|
<MsProjectSelect v-model:project="currentProject" @change="resetModule" />
|
||||||
<a-select
|
<a-select
|
||||||
v-model:model-value="protocol"
|
v-model:model-value="protocol"
|
||||||
:options="protocolOptions"
|
:options="protocolOptions"
|
||||||
class="w-[90px]"
|
class="w-[90px]"
|
||||||
@change="resetModuleAndTable"
|
@change="resetModule"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<moduleTree
|
<moduleTree
|
||||||
|
@ -42,6 +42,9 @@
|
||||||
:protocol="protocol"
|
:protocol="protocol"
|
||||||
:project-id="currentProject"
|
:project-id="currentProject"
|
||||||
:module-ids="moduleIds"
|
:module-ids="moduleIds"
|
||||||
|
:selected-apis="selectedApis"
|
||||||
|
:selected-cases="selectedCases"
|
||||||
|
:selected-scenarios="selectedScenarios"
|
||||||
@select="handleTableSelect"
|
@select="handleTableSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,8 +71,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-[12px]">
|
<div class="flex items-center gap-[12px]">
|
||||||
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
|
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
|
||||||
<a-button type="primary" @click="handleCopy">{{ t('common.copy') }}</a-button>
|
<a-button type="primary" :disabled="totalSelected === 0" @click="handleCopy">
|
||||||
<a-button type="primary" @click="handleQuote">{{ t('common.quote') }}</a-button>
|
{{ t('common.copy') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="totalSelected === 0" @click="handleQuote">
|
||||||
|
{{ t('common.quote') }}
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,9 +85,11 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectOptionData } from '@arco-design/web-vue';
|
import { SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import { MsTableDataItem } from '@/components/pure/ms-table/type';
|
||||||
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
|
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
|
||||||
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
import moduleTree from './moduleTree.vue';
|
import moduleTree from './moduleTree.vue';
|
||||||
|
@ -90,9 +99,18 @@
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
|
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||||
|
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
|
||||||
|
|
||||||
|
export interface ImportData {
|
||||||
|
api: MsTableDataItem<ApiDefinitionDetail>[];
|
||||||
|
case: MsTableDataItem<ApiCaseDetail>[];
|
||||||
|
scenario: MsTableDataItem<ApiScenarioTableItem>[];
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'copy', data: any[]): void;
|
(e: 'copy', data: ImportData): void;
|
||||||
(e: 'quote', data: any[]): void;
|
(e: 'quote', data: ImportData): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -103,20 +121,20 @@
|
||||||
});
|
});
|
||||||
const activeKey = ref<'api' | 'case' | 'scenario'>('api');
|
const activeKey = ref<'api' | 'case' | 'scenario'>('api');
|
||||||
|
|
||||||
const selectedApis = ref<any[]>([]);
|
const selectedApis = ref<MsTableDataItem<ApiDefinitionDetail>[]>([]);
|
||||||
const selectedCases = ref<any[]>([]);
|
const selectedCases = ref<MsTableDataItem<ApiCaseDetail>[]>([]);
|
||||||
const selectedScenarios = ref<any[]>([]);
|
const selectedScenarios = ref<MsTableDataItem<ApiScenarioTableItem>[]>([]);
|
||||||
const totalSelected = computed(() => {
|
const totalSelected = computed(() => {
|
||||||
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
|
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleTableSelect(ids: (string | number)[]) {
|
function handleTableSelect(data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]) {
|
||||||
if (activeKey.value === 'api') {
|
if (activeKey.value === 'api') {
|
||||||
selectedApis.value = ids;
|
selectedApis.value = data as MsTableDataItem<ApiDefinitionDetail>[];
|
||||||
} else if (activeKey.value === 'case') {
|
} else if (activeKey.value === 'case') {
|
||||||
selectedCases.value = ids;
|
selectedCases.value = data as MsTableDataItem<ApiCaseDetail>[];
|
||||||
} else if (activeKey.value === 'scenario') {
|
} else if (activeKey.value === 'scenario') {
|
||||||
selectedScenarios.value = ids;
|
selectedScenarios.value = data as MsTableDataItem<ApiScenarioTableItem>[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,9 +166,8 @@
|
||||||
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
||||||
const moduleIds = ref<(string | number)[]>([]);
|
const moduleIds = ref<(string | number)[]>([]);
|
||||||
|
|
||||||
function resetModuleAndTable() {
|
function resetModule() {
|
||||||
moduleTreeRef.value?.init(activeKey.value);
|
moduleTreeRef.value?.init(activeKey.value);
|
||||||
apiTableRef.value?.loadPage(['root']); // 这里传入根模块id,因为模块需要加载,且默认选中的就是默认模块
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModuleSelect(ids: (string | number)[], node: MsTreeNodeData) {
|
function handleModuleSelect(ids: (string | number)[], node: MsTreeNodeData) {
|
||||||
|
@ -171,33 +188,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
emit('copy', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
|
emit(
|
||||||
|
'copy',
|
||||||
|
cloneDeep({
|
||||||
|
api: selectedApis.value,
|
||||||
|
case: selectedCases.value,
|
||||||
|
scenario: selectedScenarios.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
handleCancel();
|
handleCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuote() {
|
function handleQuote() {
|
||||||
emit('quote', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
|
emit(
|
||||||
|
'quote',
|
||||||
|
cloneDeep({
|
||||||
|
api: selectedApis.value,
|
||||||
|
case: selectedCases.value,
|
||||||
|
scenario: selectedScenarios.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
handleCancel();
|
handleCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => visible.value,
|
|
||||||
(val) => {
|
|
||||||
if (val) {
|
|
||||||
// 外面使用 v-if 动态渲染时,需要在下一个tick中初始化
|
|
||||||
nextTick(() => {
|
|
||||||
resetModuleAndTable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
initProtocolList();
|
initProtocolList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 外面需要使用 v-if 动态渲染
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
// 外面使用 v-if 动态渲染时,需要在 nextTick 中执行初始化数据,因为子组件 ref 引用需要在渲染后才能获取到
|
||||||
|
moduleTreeRef.value?.init(activeKey.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -105,6 +105,7 @@
|
||||||
folderTree.value = await getScenarioModuleTree(params);
|
folderTree.value = await getScenarioModuleTree(params);
|
||||||
}
|
}
|
||||||
selectedKeys.value = [folderTree.value[0]?.id];
|
selectedKeys.value = [folderTree.value[0]?.id];
|
||||||
|
emit('select', [folderTree.value[0]?.id], folderTree.value[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
no-disable
|
no-disable
|
||||||
filter-icon-align-left
|
filter-icon-align-left
|
||||||
v-on="currentTable.propsEvent.value"
|
v-on="currentTable.propsEvent.value"
|
||||||
@selected-change="handleTableSelect"
|
|
||||||
>
|
>
|
||||||
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
|
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
|
||||||
<a-trigger
|
<a-trigger
|
||||||
|
@ -88,7 +87,7 @@
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
import { MsTableColumn, MsTableDataItem } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
@ -99,8 +98,11 @@
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||||
|
|
||||||
|
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||||
|
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
|
||||||
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
||||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||||
|
import { SelectAllEnum } from '@/enums/tableEnum';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: 'api' | 'case' | 'scenario';
|
type: 'api' | 'case' | 'scenario';
|
||||||
|
@ -108,9 +110,12 @@
|
||||||
protocol: string;
|
protocol: string;
|
||||||
projectId: string | number;
|
projectId: string | number;
|
||||||
moduleIds: (string | number)[]; // 模块 id 以及它的子孙模块 id集合
|
moduleIds: (string | number)[]; // 模块 id 以及它的子孙模块 id集合
|
||||||
|
selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; // 已选中的接口
|
||||||
|
selectedCases: MsTableDataItem<ApiCaseDetail>[]; // 已选中的用例
|
||||||
|
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; // 已选中的场景
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'select', ids: (string | number)[]): void;
|
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -161,11 +166,11 @@
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'apiTestManagement.version',
|
// title: 'apiTestManagement.version',
|
||||||
dataIndex: 'versionName',
|
// dataIndex: 'versionName',
|
||||||
width: 100,
|
// width: 100,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: 'common.tag',
|
title: 'common.tag',
|
||||||
dataIndex: 'tags',
|
dataIndex: 'tags',
|
||||||
|
@ -192,7 +197,10 @@
|
||||||
const methodFilters = ref(Object.keys(RequestMethods));
|
const methodFilters = ref(Object.keys(RequestMethods));
|
||||||
const statusFilterVisible = ref(false);
|
const statusFilterVisible = ref(false);
|
||||||
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
|
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
|
||||||
const tableSelected = ref<(string | number)[]>([]);
|
const tableSelectedData = ref<MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]>([]);
|
||||||
|
const tableSelectedKeys = computed(() => {
|
||||||
|
return tableSelectedData.value.map((e) => e.id);
|
||||||
|
});
|
||||||
// 当前展示的表格数据类型
|
// 当前展示的表格数据类型
|
||||||
const currentTable = computed(() => {
|
const currentTable = computed(() => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
|
@ -206,6 +214,40 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格单行选中事件处理
|
||||||
|
*/
|
||||||
|
function handleRowSelectChange(key: string) {
|
||||||
|
const selectedData = currentTable.value.propsRes.value.data.find((e) => e.id === key);
|
||||||
|
if (tableSelectedKeys.value.includes(key)) {
|
||||||
|
// 取消选中
|
||||||
|
tableSelectedData.value = tableSelectedData.value.filter((e) => e.id !== key);
|
||||||
|
} else if (selectedData) {
|
||||||
|
tableSelectedData.value.push(selectedData);
|
||||||
|
}
|
||||||
|
emit('select', tableSelectedData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格全选事件处理
|
||||||
|
*/
|
||||||
|
function handleSelectAllChange(v: SelectAllEnum) {
|
||||||
|
if (v === SelectAllEnum.CURRENT) {
|
||||||
|
tableSelectedData.value = currentTable.value.propsRes.value.data;
|
||||||
|
} else {
|
||||||
|
tableSelectedData.value = [];
|
||||||
|
}
|
||||||
|
emit('select', tableSelectedData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定表格事件
|
||||||
|
useApiTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||||
|
useApiTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||||
|
useCaseTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||||
|
useCaseTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||||
|
useScenarioTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||||
|
useScenarioTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||||
|
|
||||||
function loadPage(ids?: (string | number)[]) {
|
function loadPage(ids?: (string | number)[]) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 等待currentTable计算完毕再调用对应的请求
|
// 等待currentTable计算完毕再调用对应的请求
|
||||||
|
@ -226,6 +268,31 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.type,
|
||||||
|
(val) => {
|
||||||
|
switch (val) {
|
||||||
|
case 'api':
|
||||||
|
tableSelectedData.value = props.selectedApis;
|
||||||
|
break;
|
||||||
|
case 'case':
|
||||||
|
tableSelectedData.value = props.selectedCases;
|
||||||
|
break;
|
||||||
|
case 'scenario':
|
||||||
|
default:
|
||||||
|
tableSelectedData.value = props.selectedScenarios;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tableSelectedKeys.value,
|
||||||
|
(arr) => {
|
||||||
|
currentTable.value.propsRes.value.selectedKeys = new Set(arr);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function handleFilterHidden(val: boolean) {
|
function handleFilterHidden(val: boolean) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
loadPage();
|
loadPage();
|
||||||
|
@ -240,14 +307,6 @@
|
||||||
loadPage();
|
loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理表格选中
|
|
||||||
*/
|
|
||||||
function handleTableSelect(arr: (string | number)[]) {
|
|
||||||
tableSelected.value = arr;
|
|
||||||
emit('select', arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openApiDetail(id: string | number) {
|
function openApiDetail(id: string | number) {
|
||||||
let routeName: RouteRecordName;
|
let routeName: RouteRecordName;
|
||||||
const query: Record<string, any> = {};
|
const query: Record<string, any> = {};
|
||||||
|
|
|
@ -98,13 +98,14 @@
|
||||||
function handleCreateActionSelect(val: ScenarioAddStepActionType) {
|
function handleCreateActionSelect(val: ScenarioAddStepActionType) {
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case ScenarioAddStepActionType.LOOP_CONTROL:
|
case ScenarioAddStepActionType.LOOP_CONTROL:
|
||||||
if (step.value) {
|
if (step.value && props.createStepAction) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.LOOP_CONTROL,
|
type: ScenarioStepType.LOOP_CONTROL,
|
||||||
name: t('apiScenario.loopControl'),
|
name: t('apiScenario.loopControl'),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
step.value,
|
step.value,
|
||||||
|
steps.value,
|
||||||
props.createStepAction,
|
props.createStepAction,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
|
@ -119,13 +120,14 @@
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||||
if (step.value) {
|
if (step.value && props.createStepAction) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.CONDITION_CONTROL,
|
type: ScenarioStepType.CONDITION_CONTROL,
|
||||||
name: t('apiScenario.conditionControl'),
|
name: t('apiScenario.conditionControl'),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
step.value,
|
step.value,
|
||||||
|
steps.value,
|
||||||
props.createStepAction,
|
props.createStepAction,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
|
@ -140,13 +142,14 @@
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||||
if (step.value) {
|
if (step.value && props.createStepAction) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||||
name: t('apiScenario.onlyOnceControl'),
|
name: t('apiScenario.onlyOnceControl'),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
step.value,
|
step.value,
|
||||||
|
steps.value,
|
||||||
props.createStepAction,
|
props.createStepAction,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
|
@ -161,13 +164,14 @@
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ScenarioAddStepActionType.WAIT_TIME:
|
case ScenarioAddStepActionType.WAIT_TIME:
|
||||||
if (step.value) {
|
if (step.value && props.createStepAction) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.WAIT_TIME,
|
type: ScenarioStepType.WAIT_TIME,
|
||||||
name: t('apiScenario.waitTime'),
|
name: t('apiScenario.waitTime'),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
step.value,
|
step.value,
|
||||||
|
steps.value,
|
||||||
props.createStepAction,
|
props.createStepAction,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,32 +28,32 @@
|
||||||
v-if="showAddChildStep"
|
v-if="showAddChildStep"
|
||||||
:class="[
|
:class="[
|
||||||
'arco-trigger-menu-item !mx-0 !w-full',
|
'arco-trigger-menu-item !mx-0 !w-full',
|
||||||
activeCreateAction === 'addChildStep' ? 'step-tree-active-action' : '',
|
activeCreateAction === 'inside' ? 'step-tree-active-action' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleTriggerActionClick('addChildStep')"
|
@click="handleTriggerActionClick('inside')"
|
||||||
>
|
>
|
||||||
<icon-plus size="12" />
|
<icon-plus size="12" />
|
||||||
{{ t('apiScenario.addChildStep') }}
|
{{ t('apiScenario.inside') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'arco-trigger-menu-item !mx-0 !w-full',
|
'arco-trigger-menu-item !mx-0 !w-full',
|
||||||
activeCreateAction === 'insertBefore' ? 'step-tree-active-action' : '',
|
activeCreateAction === 'before' ? 'step-tree-active-action' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleTriggerActionClick('insertBefore')"
|
@click="handleTriggerActionClick('before')"
|
||||||
>
|
>
|
||||||
<icon-left size="12" />
|
<icon-left size="12" />
|
||||||
{{ t('apiScenario.insertBefore') }}
|
{{ t('apiScenario.before') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'arco-trigger-menu-item !mx-0 !w-full',
|
'arco-trigger-menu-item !mx-0 !w-full',
|
||||||
activeCreateAction === 'insertAfter' ? 'step-tree-active-action' : '',
|
activeCreateAction === 'after' ? 'step-tree-active-action' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleTriggerActionClick('insertAfter')"
|
@click="handleTriggerActionClick('after')"
|
||||||
>
|
>
|
||||||
<icon-left size="12" />
|
<icon-left size="12" />
|
||||||
{{ t('apiScenario.insertAfter') }}
|
{{ t('apiScenario.after') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,100 +2,171 @@ import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import { ScenarioStepItem } from '../stepTree.vue';
|
import { ScenarioStepItem } from '../stepTree.vue';
|
||||||
|
|
||||||
import { getGenerateId, insertNode, TreeNode } from '@/utils';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { getGenerateId, insertNodes, TreeNode } from '@/utils';
|
||||||
|
|
||||||
import { CreateStepAction } from '@/models/apiTest/scenario';
|
import { CreateStepAction } from '@/models/apiTest/scenario';
|
||||||
|
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
import { defaultStepItemCommon } from '../../config';
|
import { defaultStepItemCommon } from '../../config';
|
||||||
import steps from '@arco-design/web-vue/es/steps';
|
|
||||||
|
|
||||||
export default function useCreateActions() {
|
export default function useCreateActions() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
* 插入步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||||
|
* @param selectedKeys 选中的步骤 id 集合
|
||||||
|
* @param step 需要判断的步骤
|
||||||
|
* @param parent 需要判断的父节点
|
||||||
*/
|
*/
|
||||||
function isParentSelected(
|
function checkedIfNeed(
|
||||||
selectedKeys: (string | number)[],
|
selectedKeys: (string | number)[],
|
||||||
step: ScenarioStepItem,
|
step: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
|
||||||
parent?: TreeNode<ScenarioStepItem>
|
parent?: TreeNode<ScenarioStepItem>
|
||||||
) {
|
) {
|
||||||
if (parent && selectedKeys.includes(parent.id)) {
|
if (parent && selectedKeys.includes(parent.id)) {
|
||||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
selectedKeys.push(step.id);
|
selectedKeys = selectedKeys.concat(step.map((item) => item.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理添加子步骤、插入步骤前/后操作
|
* 处理添加子步骤、插入步骤前/后操作-创建单个步骤
|
||||||
|
* @param defaultStepInfo 创建传入的默认步骤信息
|
||||||
|
* @param step 目标步骤
|
||||||
|
* @param steps 顶层步骤列表
|
||||||
|
* @param createStepAction 创建步骤操作类型
|
||||||
|
* @param selectedKeys 选中的步骤 id 集合
|
||||||
*/
|
*/
|
||||||
function handleCreateStep(
|
function handleCreateStep(
|
||||||
defaultStepInfo: ScenarioStepItem,
|
defaultStepInfo: ScenarioStepItem,
|
||||||
step: ScenarioStepItem,
|
step: ScenarioStepItem,
|
||||||
|
steps: ScenarioStepItem[],
|
||||||
createStepAction: CreateStepAction,
|
createStepAction: CreateStepAction,
|
||||||
selectedKeys: (string | number)[]
|
selectedKeys: (string | number)[]
|
||||||
) {
|
) {
|
||||||
|
const newStep = {
|
||||||
|
...cloneDeep(defaultStepItemCommon),
|
||||||
|
...defaultStepInfo,
|
||||||
|
id: getGenerateId(),
|
||||||
|
};
|
||||||
switch (createStepAction) {
|
switch (createStepAction) {
|
||||||
case 'addChildStep':
|
case 'inside':
|
||||||
const id = getGenerateId();
|
newStep.order = step.children ? step.children.length : 0;
|
||||||
if (step.children) {
|
|
||||||
step.children.push({
|
|
||||||
...cloneDeep(defaultStepItemCommon),
|
|
||||||
...defaultStepInfo,
|
|
||||||
id,
|
|
||||||
order: step.children.length + 1,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
step.children = [
|
|
||||||
{
|
|
||||||
...cloneDeep(defaultStepItemCommon),
|
|
||||||
...defaultStepInfo,
|
|
||||||
id,
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (selectedKeys.includes(step.id)) {
|
|
||||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
|
||||||
selectedKeys.push(id);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'insertBefore':
|
case 'before':
|
||||||
insertNode<ScenarioStepItem>(
|
newStep.order = step.order;
|
||||||
step.children || steps.value,
|
|
||||||
step.id,
|
|
||||||
{
|
|
||||||
...cloneDeep(defaultStepItemCommon),
|
|
||||||
...defaultStepInfo,
|
|
||||||
id: getGenerateId(),
|
|
||||||
order: step.order,
|
|
||||||
},
|
|
||||||
'before',
|
|
||||||
(parent) => isParentSelected(selectedKeys, step, parent),
|
|
||||||
'id'
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'insertAfter':
|
case 'after':
|
||||||
insertNode<ScenarioStepItem>(
|
default:
|
||||||
step.children || steps.value,
|
newStep.order = step.order + 1;
|
||||||
step.id,
|
break;
|
||||||
{
|
}
|
||||||
...cloneDeep(defaultStepItemCommon),
|
insertNodes<ScenarioStepItem>(
|
||||||
...defaultStepInfo,
|
step.parent?.children || steps,
|
||||||
id: getGenerateId(),
|
step.id,
|
||||||
order: step.order + 1,
|
newStep,
|
||||||
},
|
createStepAction,
|
||||||
'after',
|
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
|
||||||
(parent) => isParentSelected(selectedKeys, step, parent),
|
'id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
'id'
|
/**
|
||||||
);
|
* 组装插入操作的步骤信息
|
||||||
|
* @param newSteps 新步骤信息集合
|
||||||
|
* @param type 需要插入的步骤类型
|
||||||
|
* @param startOrder 步骤最开始的排序序号
|
||||||
|
*/
|
||||||
|
function buildInsertStepInfos(
|
||||||
|
newSteps: Record<string, any>[],
|
||||||
|
type: ScenarioStepType,
|
||||||
|
startOrder: number
|
||||||
|
): ScenarioStepItem[] {
|
||||||
|
let name: string;
|
||||||
|
switch (type) {
|
||||||
|
case ScenarioStepType.LOOP_CONTROL:
|
||||||
|
name = t('apiScenario.loopControl');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.CONDITION_CONTROL:
|
||||||
|
name = t('apiScenario.conditionControl');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.ONLY_ONCE_CONTROL:
|
||||||
|
name = t('apiScenario.onlyOnceControl');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.WAIT_TIME:
|
||||||
|
name = t('apiScenario.waitTime');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.QUOTE_API:
|
||||||
|
name = t('apiScenario.quoteApi');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.COPY_API:
|
||||||
|
name = t('apiScenario.copyApi');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.QUOTE_CASE:
|
||||||
|
name = t('apiScenario.quoteCase');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.COPY_CASE:
|
||||||
|
name = t('apiScenario.copyCase');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.QUOTE_SCENARIO:
|
||||||
|
name = t('apiScenario.quoteScenario');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.COPY_SCENARIO:
|
||||||
|
name = t('apiScenario.copyScenario');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.CUSTOM_API:
|
||||||
|
name = t('apiScenario.customApi');
|
||||||
|
break;
|
||||||
|
case ScenarioStepType.SCRIPT_OPERATION:
|
||||||
|
name = t('apiScenario.scriptOperation');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
return newSteps.map((item, index) => {
|
||||||
|
return {
|
||||||
|
...cloneDeep(defaultStepItemCommon),
|
||||||
|
...item,
|
||||||
|
id: getGenerateId(),
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
order: startOrder + index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理添加子步骤、插入步骤前/后操作-创建多个步骤
|
||||||
|
* @param step 目标步骤
|
||||||
|
* @param readyInsertSteps 待插入的步骤信息数组(需要先buildInsertStepInfos得到构建后的步骤信息)
|
||||||
|
* @param steps 顶层步骤列表
|
||||||
|
* @param createStepAction 创建步骤操作类型
|
||||||
|
* @param type 需要插入的步骤类型
|
||||||
|
* @param selectedKeys 选中的步骤 id 集合
|
||||||
|
*/
|
||||||
|
function handleCreateSteps(
|
||||||
|
step: ScenarioStepItem,
|
||||||
|
readyInsertSteps: ScenarioStepItem[],
|
||||||
|
steps: ScenarioStepItem[],
|
||||||
|
createStepAction: CreateStepAction,
|
||||||
|
selectedKeys: (string | number)[]
|
||||||
|
) {
|
||||||
|
insertNodes<ScenarioStepItem>(
|
||||||
|
step.parent?.children || steps,
|
||||||
|
step.id,
|
||||||
|
readyInsertSteps,
|
||||||
|
createStepAction,
|
||||||
|
undefined,
|
||||||
|
'id'
|
||||||
|
);
|
||||||
|
checkedIfNeed(selectedKeys, readyInsertSteps, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleCreateStep,
|
handleCreateStep,
|
||||||
isParentSelected,
|
buildInsertStepInfos,
|
||||||
|
handleCreateSteps,
|
||||||
|
checkedIfNeed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@
|
||||||
if (val.length === 0) {
|
if (val.length === 0) {
|
||||||
checkedAll.value = false;
|
checkedAll.value = false;
|
||||||
indeterminate.value = false;
|
indeterminate.value = false;
|
||||||
} else if (val.length === stepInfo.value.steps.length) {
|
} else if (val.length === totalStepCount.value) {
|
||||||
checkedAll.value = true;
|
checkedAll.value = true;
|
||||||
indeterminate.value = false;
|
indeterminate.value = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
>
|
>
|
||||||
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
|
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
|
||||||
<MsIcon
|
<MsIcon
|
||||||
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
|
:type="step.expanded ? 'icon-icon_split-turn-down-left' : 'icon-icon_split_turn-down_arrow'"
|
||||||
:size="14"
|
:size="14"
|
||||||
/>
|
/>
|
||||||
{{ step.children?.length || 0 }}
|
{{ step.children?.length || 0 }}
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #extraEnd="step">
|
<template #extraEnd="step">
|
||||||
<executeStatus v-if="step.status" :status="step.status" size="small" />
|
<executeStatus v-if="step.executeStatus" :status="step.executeStatus" size="small" />
|
||||||
</template>
|
</template>
|
||||||
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
|
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
|
||||||
<div
|
<div
|
||||||
|
@ -193,7 +193,12 @@
|
||||||
:request="activeStep?.request"
|
:request="activeStep?.request"
|
||||||
@add-step="addCustomApiStep"
|
@add-step="addCustomApiStep"
|
||||||
/>
|
/>
|
||||||
<importApiDrawer v-if="importApiDrawerVisible" v-model:visible="importApiDrawerVisible" />
|
<importApiDrawer
|
||||||
|
v-if="importApiDrawerVisible"
|
||||||
|
v-model:visible="importApiDrawerVisible"
|
||||||
|
@copy="handleImportApiApply('copy', $event)"
|
||||||
|
@quote="handleImportApiApply('quote', $event)"
|
||||||
|
/>
|
||||||
<scriptOperationDrawer
|
<scriptOperationDrawer
|
||||||
v-if="scriptOperationDrawerVisible"
|
v-if="scriptOperationDrawerVisible"
|
||||||
v-model:visible="scriptOperationDrawerVisible"
|
v-model:visible="scriptOperationDrawerVisible"
|
||||||
|
@ -242,6 +247,7 @@
|
||||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
import executeStatus from '../common/executeStatus.vue';
|
import executeStatus from '../common/executeStatus.vue';
|
||||||
|
import { ImportData } from '../common/importApiDrawer/index.vue';
|
||||||
import stepType from '../common/stepType.vue';
|
import stepType from '../common/stepType.vue';
|
||||||
import createStepActions from './createAction/createStepActions.vue';
|
import createStepActions from './createAction/createStepActions.vue';
|
||||||
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
|
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
|
||||||
|
@ -254,7 +260,15 @@
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import { deleteNode, findNodeByKey, getGenerateId, handleTreeDragDrop, insertNode, mapTree, TreeNode } from '@/utils';
|
import {
|
||||||
|
deleteNode,
|
||||||
|
findNodeByKey,
|
||||||
|
getGenerateId,
|
||||||
|
handleTreeDragDrop,
|
||||||
|
insertNodes,
|
||||||
|
mapTree,
|
||||||
|
TreeNode,
|
||||||
|
} from '@/utils';
|
||||||
|
|
||||||
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||||
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
|
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
|
||||||
|
@ -278,7 +292,7 @@
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
method?: RequestMethods;
|
method?: RequestMethods;
|
||||||
status?: ScenarioExecuteStatus;
|
executeStatus?: ScenarioExecuteStatus;
|
||||||
num?: number; // 详情或者引用的类型才有
|
num?: number; // 详情或者引用的类型才有
|
||||||
// 引用类型专有字段
|
// 引用类型专有字段
|
||||||
belongProjectId?: string;
|
belongProjectId?: string;
|
||||||
|
@ -293,7 +307,7 @@
|
||||||
checked: boolean; // 是否选中
|
checked: boolean; // 是否选中
|
||||||
expanded: boolean; // 是否展开
|
expanded: boolean; // 是否展开
|
||||||
createActionsVisible?: boolean; // 是否展示创建步骤下拉
|
createActionsVisible?: boolean; // 是否展示创建步骤下拉
|
||||||
parent?: ScenarioStepItem | ScenarioStepItem[]; // 父级节点,第一层的父级节点为undefined
|
parent?: ScenarioStepItem; // 父级节点,第一层的父级节点为undefined
|
||||||
loopNum: number;
|
loopNum: number;
|
||||||
loopType: 'num' | 'while' | 'forEach';
|
loopType: 'num' | 'while' | 'forEach';
|
||||||
loopSpace: number;
|
loopSpace: number;
|
||||||
|
@ -376,7 +390,7 @@
|
||||||
/**
|
/**
|
||||||
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||||
*/
|
*/
|
||||||
function isParentSelected(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
|
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
|
||||||
if (parent && selectedKeys.value.includes(parent.id)) {
|
if (parent && selectedKeys.value.includes(parent.id)) {
|
||||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
selectedKeys.value.push(step.id);
|
selectedKeys.value.push(step.id);
|
||||||
|
@ -455,7 +469,7 @@
|
||||||
switch (item.eventTag) {
|
switch (item.eventTag) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
const id = getGenerateId();
|
const id = getGenerateId();
|
||||||
insertNode<ScenarioStepItem>(
|
insertNodes<ScenarioStepItem>(
|
||||||
steps.value,
|
steps.value,
|
||||||
node.id,
|
node.id,
|
||||||
{
|
{
|
||||||
|
@ -472,7 +486,7 @@
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
'after',
|
'after',
|
||||||
isParentSelected,
|
checkedIfNeed,
|
||||||
'id'
|
'id'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -574,7 +588,6 @@
|
||||||
customApiDrawerVisible.value = true;
|
customApiDrawerVisible.value = true;
|
||||||
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
|
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
|
||||||
activeStep.value = step;
|
activeStep.value = step;
|
||||||
console.log('activeStep', activeStep.value);
|
|
||||||
scriptOperationDrawerVisible.value = true;
|
scriptOperationDrawerVisible.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,12 +624,58 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { handleCreateStep } = useCreateActions();
|
const { handleCreateStep, handleCreateSteps, buildInsertStepInfos } = useCreateActions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理导入系统请求
|
||||||
|
* @param type 导入类型
|
||||||
|
* @param data 导入数据
|
||||||
|
*/
|
||||||
|
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
|
||||||
|
let order = steps.value.length + 1;
|
||||||
|
if (activeStep.value && activeCreateAction.value) {
|
||||||
|
switch (activeCreateAction.value) {
|
||||||
|
case 'inside':
|
||||||
|
order = activeStep.value.children ? activeStep.value.children.length : 0;
|
||||||
|
break;
|
||||||
|
case 'before':
|
||||||
|
order = activeStep.value.order;
|
||||||
|
break;
|
||||||
|
case 'after':
|
||||||
|
order = activeStep.value.order + 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const insertApiSteps = buildInsertStepInfos(
|
||||||
|
data.api,
|
||||||
|
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API,
|
||||||
|
order
|
||||||
|
);
|
||||||
|
const insertCaseSteps = buildInsertStepInfos(
|
||||||
|
data.case,
|
||||||
|
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE,
|
||||||
|
order + insertApiSteps.length
|
||||||
|
);
|
||||||
|
const insertScenarioSteps = buildInsertStepInfos(
|
||||||
|
data.scenario,
|
||||||
|
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO,
|
||||||
|
order + insertApiSteps.length + insertCaseSteps.length
|
||||||
|
);
|
||||||
|
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
|
||||||
|
if (activeStep.value && activeCreateAction.value) {
|
||||||
|
handleCreateSteps(activeStep.value, insertSteps, steps.value, activeCreateAction.value, selectedKeys.value);
|
||||||
|
} else {
|
||||||
|
steps.value = steps.value.concat(insertSteps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加自定义 API 步骤
|
* 添加自定义 API 步骤
|
||||||
*/
|
*/
|
||||||
function addCustomApiStep(request: RequestParam) {
|
function addCustomApiStep(request: RequestParam) {
|
||||||
if (activeStep.value) {
|
if (activeStep.value && activeCreateAction.value) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.CUSTOM_API,
|
type: ScenarioStepType.CUSTOM_API,
|
||||||
|
@ -624,6 +683,7 @@
|
||||||
request: cloneDeep(request),
|
request: cloneDeep(request),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
activeStep.value,
|
activeStep.value,
|
||||||
|
steps.value,
|
||||||
activeCreateAction.value,
|
activeCreateAction.value,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
|
@ -643,7 +703,7 @@
|
||||||
* 添加脚本操作步骤
|
* 添加脚本操作步骤
|
||||||
*/
|
*/
|
||||||
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
|
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
|
||||||
if (activeStep.value) {
|
if (activeStep.value && activeCreateAction.value) {
|
||||||
handleCreateStep(
|
handleCreateStep(
|
||||||
{
|
{
|
||||||
type: ScenarioStepType.SCRIPT_OPERATION,
|
type: ScenarioStepType.SCRIPT_OPERATION,
|
||||||
|
@ -651,10 +711,10 @@
|
||||||
script: cloneDeep(scriptProcessor),
|
script: cloneDeep(scriptProcessor),
|
||||||
} as ScenarioStepItem,
|
} as ScenarioStepItem,
|
||||||
activeStep.value,
|
activeStep.value,
|
||||||
|
steps.value,
|
||||||
activeCreateAction.value,
|
activeCreateAction.value,
|
||||||
selectedKeys.value
|
selectedKeys.value
|
||||||
);
|
);
|
||||||
console.log('activeStep', activeStep.value);
|
|
||||||
} else {
|
} else {
|
||||||
steps.value.push({
|
steps.value.push({
|
||||||
...cloneDeep(defaultStepItemCommon),
|
...cloneDeep(defaultStepItemCommon),
|
||||||
|
@ -668,7 +728,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理文件夹树节点拖拽事件
|
* 释放允许拖拽步骤到释放的节点内
|
||||||
|
* @param dropNode 释放节点
|
||||||
|
*/
|
||||||
|
function isAllowDropInside(dropNode: MsTreeNodeData) {
|
||||||
|
return [
|
||||||
|
ScenarioStepType.LOOP_CONTROL,
|
||||||
|
ScenarioStepType.CONDITION_CONTROL,
|
||||||
|
ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||||
|
].includes(dropNode.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤节点拖拽事件
|
||||||
* @param tree 树数据
|
* @param tree 树数据
|
||||||
* @param dragNode 拖拽节点
|
* @param dragNode 拖拽节点
|
||||||
* @param dropNode 释放节点
|
* @param dropNode 释放节点
|
||||||
|
@ -681,19 +753,38 @@
|
||||||
dropPosition: number
|
dropPosition: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
if (dropPosition === 0 && !isAllowDropInside(dropNode)) {
|
||||||
|
// Message.error(t('apiScenario.notAllowDropInside')); TODO:不允许释放提示
|
||||||
|
return;
|
||||||
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
mapTree(dragNode.children || [], (e) => {
|
||||||
|
offspringIds.push(e.id);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
const stepIdAndOffspringIds = [dragNode.id, ...offspringIds];
|
||||||
if (dropPosition === 0) {
|
if (dropPosition === 0) {
|
||||||
// 拖拽到节点内
|
// 拖拽到节点内
|
||||||
if (selectedKeys.value.includes(dropNode.id)) {
|
if (selectedKeys.value.includes(dropNode.id)) {
|
||||||
// 释放位置的节点已选中,则需要把拖动的节点也需要选中(因为父级选中子级也会展示选中状态)
|
// 释放位置的节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
selectedKeys.value.push(dragNode.id);
|
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
||||||
}
|
}
|
||||||
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
|
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
|
||||||
// 释放位置的节点的父节点已选中,则需要把拖动的节点也需要选中(因为父级选中子级也会展示选中状态)
|
// 释放位置的节点的父节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
selectedKeys.value.push(dragNode.id);
|
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
||||||
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
|
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
|
||||||
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点从选中的节点中移除
|
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点及其子孙节点从选中的节点中移除
|
||||||
selectedKeys.value = selectedKeys.value.filter((e) => e !== dragNode.id);
|
selectedKeys.value = selectedKeys.value.filter((e) => {
|
||||||
|
for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
|
||||||
|
const id = stepIdAndOffspringIds[i];
|
||||||
|
if (e === id) {
|
||||||
|
stepIdAndOffspringIds.splice(i, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
|
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
|
||||||
if (dragResult) {
|
if (dragResult) {
|
||||||
|
|
|
@ -51,6 +51,16 @@
|
||||||
allow-search
|
allow-search
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('apiScenario.scenarioLevel')">
|
||||||
|
<a-select v-model:model-value="scenario.priority" :placeholder="t('common.pleaseSelect')">
|
||||||
|
<template #label>
|
||||||
|
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="scenario.priority" /></span>
|
||||||
|
</template>
|
||||||
|
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||||
|
<caseLevel :case-level="item.label as CaseLevel" />
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item :label="t('apiScenario.status')" class="mb-[16px]">
|
<a-form-item :label="t('apiScenario.status')" class="mb-[16px]">
|
||||||
<a-select
|
<a-select
|
||||||
v-model:model-value="scenario.status"
|
v-model:model-value="scenario.status"
|
||||||
|
@ -120,6 +130,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
|
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||||
|
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
@ -128,6 +140,8 @@
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||||
|
|
||||||
// 组成部分异步导入
|
// 组成部分异步导入
|
||||||
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
|
||||||
const params = defineAsyncComponent(() => import('../components/params.vue'));
|
const params = defineAsyncComponent(() => import('../components/params.vue'));
|
||||||
|
|
|
@ -15,10 +15,12 @@
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</MsEditableTab>
|
</MsEditableTab>
|
||||||
<div class="flex items-center gap-[8px]">
|
<div v-if="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]">
|
||||||
|
<environmentSelect />
|
||||||
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
|
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<!-- <executeButton /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-divider class="!my-0" />
|
<a-divider class="!my-0" />
|
||||||
|
@ -79,6 +81,8 @@
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
import scenarioModuleTree from './components/scenarioModuleTree.vue';
|
import scenarioModuleTree from './components/scenarioModuleTree.vue';
|
||||||
import { ScenarioStepInfo } from './components/step/index.vue';
|
import { ScenarioStepInfo } from './components/step/index.vue';
|
||||||
|
import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
|
||||||
|
// import executeButton from '@/views/api-test/components/executeButton.vue';
|
||||||
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
|
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
|
||||||
|
|
||||||
import { getTrashModuleCount } from '@/api/modules/api-test/scenario';
|
import { getTrashModuleCount } from '@/api/modules/api-test/scenario';
|
||||||
|
@ -114,6 +118,7 @@
|
||||||
isNew: true,
|
isNew: true,
|
||||||
name: '',
|
name: '',
|
||||||
moduleId: 'root',
|
moduleId: 'root',
|
||||||
|
priority: 'P0',
|
||||||
stepInfo: {
|
stepInfo: {
|
||||||
id: new Date().getTime(),
|
id: new Date().getTime(),
|
||||||
steps: [],
|
steps: [],
|
||||||
|
|
|
@ -105,9 +105,9 @@ export default {
|
||||||
'apiScenario.crossProject': '跨项目',
|
'apiScenario.crossProject': '跨项目',
|
||||||
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
|
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
|
||||||
'apiScenario.collapseStepTip': '折叠 {count} 个子步骤',
|
'apiScenario.collapseStepTip': '折叠 {count} 个子步骤',
|
||||||
'apiScenario.addChildStep': '添加子步骤',
|
'apiScenario.inside': '添加子步骤',
|
||||||
'apiScenario.insertBefore': '在之前插入步骤',
|
'apiScenario.before': '在之前插入步骤',
|
||||||
'apiScenario.insertAfter': '在之后插入步骤',
|
'apiScenario.after': '在之后插入步骤',
|
||||||
'apiScenario.num': '次数',
|
'apiScenario.num': '次数',
|
||||||
'apiScenario.space': '间隔(ms)',
|
'apiScenario.space': '间隔(ms)',
|
||||||
'apiScenario.overTime': '超时(ms)',
|
'apiScenario.overTime': '超时(ms)',
|
||||||
|
@ -132,6 +132,7 @@ export default {
|
||||||
'apiScenario.topStep': '一级步骤',
|
'apiScenario.topStep': '一级步骤',
|
||||||
'apiScenario.allStep': '所有子步骤',
|
'apiScenario.allStep': '所有子步骤',
|
||||||
'apiScenario.saveAsApi': '保存为新接口',
|
'apiScenario.saveAsApi': '保存为新接口',
|
||||||
|
'apiScenario.scenarioLevel': '场景等级',
|
||||||
// 执行历史
|
// 执行历史
|
||||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||||
'apiScenario.executeHistory.num': '序号',
|
'apiScenario.executeHistory.num': '序号',
|
||||||
|
|
Loading…
Reference in New Issue