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