feat(接口场景): 场景步骤 90%&导入系统请求API&CASE完成
This commit is contained in:
parent
09454001bd
commit
cef0f5895f
|
@ -179,7 +179,7 @@ export function updateDefinition(data: ApiDefinitionUpdateParams) {
|
|||
}
|
||||
|
||||
// 获取接口定义详情
|
||||
export function getDefinitionDetail(id: string) {
|
||||
export function getDefinitionDetail(id: string | number) {
|
||||
return MSR.get<ApiDefinitionDetail>({ url: GetDefinitionDetailUrl, params: id });
|
||||
}
|
||||
|
||||
|
|
|
@ -416,16 +416,17 @@
|
|||
background-color: rgb(var(--primary-7));
|
||||
}
|
||||
}
|
||||
.arco-checkbox-disabled,
|
||||
.arco-checkbox-disabled:hover {
|
||||
.arco-checkbox-disabled.arco-checkbox-checked,
|
||||
.arco-checkbox-disabled.arco-checkbox-checked:hover {
|
||||
.arco-checkbox-icon {
|
||||
@apply bg-white;
|
||||
@apply bg-white text-white;
|
||||
|
||||
border: 1px solid var(--color-text-input-border);
|
||||
border: none;
|
||||
background: rgb(var(--primary-2)) !important;
|
||||
}
|
||||
.arco-checkbox-icon-check {
|
||||
background-color: rgb(var(--primary-2)) !important;
|
||||
}
|
||||
}
|
||||
.arco-checkbox-disabled.arco-checkbox-checked .arco-checkbox-icon {
|
||||
background: rgb(var(--primary-2)) !important ;
|
||||
}
|
||||
|
||||
/** radio **/
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<slot name="title">
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-tooltip :content="props.title">
|
||||
<a-tooltip :disabled="!props.title" :content="props.title">
|
||||
<span> {{ characterLimit(props.title) }}</span>
|
||||
</a-tooltip>
|
||||
|
||||
|
@ -161,7 +161,7 @@
|
|||
showFullScreen: false,
|
||||
okPermission: () => [], // 确认按钮权限
|
||||
});
|
||||
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']);
|
||||
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -191,6 +191,7 @@
|
|||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
emit('update:visible', false);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const resizing = ref(false); // 是否正在拖拽
|
||||
|
|
|
@ -29,8 +29,9 @@
|
|||
<template #title>
|
||||
<SelectALL
|
||||
v-if="attrs.selectorType === 'checkbox'"
|
||||
:total="selectTotal"
|
||||
:current="selectCurrent"
|
||||
:total="attrs.showPagination ? (attrs.msPagination as MsPaginationI).total : (attrs.data as MsTableDataItem<TableData>[]).length"
|
||||
:selected-keys="props.selectedKeys"
|
||||
:current-data="attrs.data as Record<string,any>[]"
|
||||
:show-select-all="!!attrs.showPagination && props.showSelectorAll"
|
||||
:disabled="(attrs.data as []).length === 0"
|
||||
@change="handleSelectAllChange"
|
||||
|
@ -208,17 +209,17 @@
|
|||
class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center"
|
||||
:class="{ 'justify-between': showBatchAction }"
|
||||
>
|
||||
<span v-if="!props.actionConfig && selectCurrent > 0" class="title text-[var(--color-text-2)]"
|
||||
>{{ t('msTable.batch.selected', { count: selectCurrent }) }}
|
||||
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">{{
|
||||
t('msTable.batch.clear')
|
||||
}}</a-button></span
|
||||
>
|
||||
<span v-if="!props.actionConfig && selectedCount > 0" class="title text-[var(--color-text-2)]">
|
||||
{{ t('msTable.batch.selected', { count: selectedCount }) }}
|
||||
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">
|
||||
{{ t('msTable.batch.clear') }}
|
||||
</a-button>
|
||||
</span>
|
||||
<div class="flex flex-grow">
|
||||
<batch-action
|
||||
v-if="showBatchAction"
|
||||
class="flex-1"
|
||||
:select-row-count="selectCurrent"
|
||||
:select-row-count="selectedCount"
|
||||
:action-config="props.actionConfig"
|
||||
@batch-action="handleBatchAction"
|
||||
@clear="emit('clearSelector')"
|
||||
|
@ -227,7 +228,6 @@
|
|||
<div class="min-w-[500px]">
|
||||
<ms-pagination
|
||||
v-if="!!attrs.showPagination"
|
||||
v-show="props.selectorStatus !== SelectAllEnum.CURRENT"
|
||||
size="small"
|
||||
v-bind="(attrs.msPagination as MsPaginationI)"
|
||||
hide-on-single-page
|
||||
|
@ -329,31 +329,6 @@
|
|||
(e: 'moduleChange'): void;
|
||||
}>();
|
||||
const attrs = useAttrs();
|
||||
// 全选按钮-总条数
|
||||
const selectTotal = computed(() => {
|
||||
const { selectorStatus } = props;
|
||||
if (!attrs.showPagination) {
|
||||
// 不展示分页时直接返回total
|
||||
return (attrs.data as MsTableDataItem<TableData>[]).length;
|
||||
}
|
||||
if (selectorStatus === SelectAllEnum.CURRENT) {
|
||||
const { pageSize, total } = attrs.msPagination as MsPaginationI;
|
||||
if (pageSize > total) {
|
||||
return total;
|
||||
}
|
||||
return pageSize;
|
||||
}
|
||||
return (attrs.msPagination as MsPaginationI)?.total || appStore.pageSize;
|
||||
});
|
||||
|
||||
// 全选按钮-当前的条数
|
||||
const selectCurrent = computed(() => {
|
||||
const { selectorStatus, excludeKeys, selectedKeys } = props;
|
||||
if (selectorStatus === SelectAllEnum.ALL) {
|
||||
return selectTotal.value - excludeKeys.size;
|
||||
}
|
||||
return selectedKeys.size;
|
||||
});
|
||||
|
||||
// 编辑按钮的Active状态
|
||||
const editActiveKey = ref<string>('');
|
||||
|
@ -483,8 +458,15 @@
|
|||
emit('pageSizeChange', v);
|
||||
};
|
||||
|
||||
const selectedCount = computed(() => {
|
||||
if (props.selectorStatus === SelectAllEnum.ALL && attrs.msPagination) {
|
||||
return (attrs.msPagination as MsPaginationI).total - props.excludeKeys.size;
|
||||
}
|
||||
return props.selectedKeys.size;
|
||||
});
|
||||
|
||||
const showBatchAction = computed(() => {
|
||||
return selectCurrent.value > 0 && attrs.selectable;
|
||||
return selectedCount.value > 0 && attrs.selectable;
|
||||
});
|
||||
|
||||
const handleBatchAction = (value: BatchActionParams) => {
|
||||
|
@ -493,7 +475,7 @@
|
|||
selectedIds: Array.from(selectedKeys),
|
||||
excludeIds: Array.from(excludeKeys),
|
||||
selectAll: selectorStatus === SelectAllEnum.ALL,
|
||||
currentSelectCount: selectCurrent.value,
|
||||
currentSelectCount: selectedCount.value,
|
||||
params: {
|
||||
...(attrs.msPagination as MsPaginationI),
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="ms-table-select-all">
|
||||
<a-checkbox
|
||||
v-model="checked"
|
||||
v-model:model-value="checked"
|
||||
:disabled="props.disabled"
|
||||
class="text-base"
|
||||
:indeterminate="indeterminate"
|
||||
|
@ -20,14 +20,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchEffect } from 'vue';
|
||||
|
||||
import MsIcon from '../ms-icon-font/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { SelectAllEnum } from '@/enums/tableEnum';
|
||||
|
||||
import { MsTableDataItem } from './type';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -35,7 +35,13 @@
|
|||
}>();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ current: number; total: number; showSelectAll: boolean; disabled: boolean }>(),
|
||||
defineProps<{
|
||||
selectedKeys: Set<string>;
|
||||
total: number;
|
||||
currentData: MsTableDataItem<Record<string, any>>[];
|
||||
showSelectAll: boolean;
|
||||
disabled: boolean;
|
||||
}>(),
|
||||
{
|
||||
current: 0,
|
||||
total: 0,
|
||||
|
@ -44,20 +50,17 @@
|
|||
}
|
||||
);
|
||||
|
||||
const checked = ref(false);
|
||||
const indeterminate = ref(false);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.current === 0) {
|
||||
checked.value = false;
|
||||
indeterminate.value = false;
|
||||
} else if (props.current < props.total) {
|
||||
checked.value = false;
|
||||
indeterminate.value = true;
|
||||
} else if (props.current === props.total) {
|
||||
checked.value = true;
|
||||
indeterminate.value = false;
|
||||
}
|
||||
const checked = computed({
|
||||
get: () => {
|
||||
return props.selectedKeys.size === props.total;
|
||||
},
|
||||
set: (value) => {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const indeterminate = computed(() => {
|
||||
// 已选中的数量大于 0 且小于总数时是半选状态
|
||||
return props.selectedKeys.size > 0 && props.selectedKeys.size < props.total;
|
||||
});
|
||||
|
||||
const handleSelect = (v: string | number | Record<string, any> | undefined) => {
|
||||
|
@ -65,9 +68,11 @@
|
|||
};
|
||||
|
||||
const handleCheckChange = () => {
|
||||
if (checked.value) {
|
||||
if (props.currentData.some((item) => !props.selectedKeys.has(item.id))) {
|
||||
// 当前页有数据没有勾选上,此时点击全选按钮代表全部选中
|
||||
handleSelect(SelectAllEnum.CURRENT);
|
||||
} else {
|
||||
// 否则是当前页全部数据已勾选,此时点击全选按钮代表取消当前页面数据勾选
|
||||
handleSelect(SelectAllEnum.NONE);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@ export type MsTableErrorStatus = boolean | 'error' | 'empty';
|
|||
export type MsTableDataItem<T> = T & {
|
||||
updateTime?: string | number | null;
|
||||
createTime?: string | number | null;
|
||||
children?: MsTableDataItem<T>[];
|
||||
} & TableData;
|
||||
// 表格属性
|
||||
export interface MsTableProps<T> {
|
||||
|
|
|
@ -296,8 +296,17 @@ export default function useTableProps<T>(
|
|||
|
||||
// 重置选择器
|
||||
const resetSelector = (isNone = true) => {
|
||||
propsRes.value.selectedKeys.clear();
|
||||
propsRes.value.excludeKeys.clear();
|
||||
if (propsRes.value.selectorStatus === SelectAllEnum.ALL) {
|
||||
// 当前是跨页全部选中状态,则取消当前页的选中项
|
||||
propsRes.value.data.forEach((item) => {
|
||||
propsRes.value.selectedKeys.delete(item.id);
|
||||
propsRes.value.excludeKeys.add(item.id);
|
||||
});
|
||||
} else {
|
||||
// 当前是当前页选中状态,则清空选中项
|
||||
propsRes.value.selectedKeys.clear();
|
||||
propsRes.value.excludeKeys.clear();
|
||||
}
|
||||
if (isNone) {
|
||||
propsRes.value.selectorStatus = SelectAllEnum.NONE;
|
||||
}
|
||||
|
@ -311,26 +320,18 @@ export default function useTableProps<T>(
|
|||
// 如果是全选状态,返回总数减去排除的数量
|
||||
return msPagination.total - excludeKeys.size;
|
||||
}
|
||||
// if (selectorStatus === SelectAllEnum.NONE) {
|
||||
// 如果是全不选状态,返回选中的数量
|
||||
return selectedKeys.size;
|
||||
// }
|
||||
// if (selectorStatus === SelectAllEnum.CURRENT) {
|
||||
// // 如果是当前页状态,返回当前页减去排除的数量
|
||||
// return msPagination.pageSize - excludeKeys.size;
|
||||
// }
|
||||
}
|
||||
};
|
||||
const collectIds = (data, rowKey: string, selectedKeys: Set<string>) => {
|
||||
data.forEach((item: any) => {
|
||||
if (item[rowKey] && !selectedKeys.has(item[rowKey])) {
|
||||
selectedKeys.add(item[rowKey]);
|
||||
const collectIds = (data: MsTableDataItem<T>[], rowKey: string) => {
|
||||
data.forEach((item: MsTableDataItem<T>) => {
|
||||
if (item[rowKey] && !propsRes.value.selectedKeys.has(item[rowKey])) {
|
||||
propsRes.value.selectedKeys.add(item[rowKey]);
|
||||
}
|
||||
if (item.children) {
|
||||
collectIds(item.children, rowKey, selectedKeys);
|
||||
collectIds(item.children, rowKey);
|
||||
}
|
||||
});
|
||||
return selectedKeys;
|
||||
};
|
||||
|
||||
// 获取表格请求参数
|
||||
|
@ -398,19 +399,19 @@ export default function useTableProps<T>(
|
|||
},
|
||||
// 重置筛选
|
||||
clearSelector: () => {
|
||||
propsRes.value.selectorStatus = SelectAllEnum.NONE; // 重置选择器状态
|
||||
resetSelector();
|
||||
},
|
||||
|
||||
// 表格SelectAll change
|
||||
selectAllChange: (v: SelectAllEnum) => {
|
||||
propsRes.value.selectorStatus = v;
|
||||
const { data, rowKey, selectedKeys } = propsRes.value;
|
||||
const { data, rowKey } = propsRes.value;
|
||||
if (v === SelectAllEnum.NONE) {
|
||||
// 清空选中项
|
||||
resetSelector();
|
||||
} else {
|
||||
resetSelector(false);
|
||||
propsRes.value.selectedKeys = collectIds(data, rowKey, selectedKeys);
|
||||
collectIds(data as MsTableDataItem<T>[], rowKey);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -417,9 +417,9 @@ export function insertNodes<T>(
|
|||
}
|
||||
if (typeof customFunc === 'function') {
|
||||
if (Array.isArray(newNodes)) {
|
||||
newNodes.forEach((newNode) => customFunc(newNode, parent || node.parent));
|
||||
newNodes.forEach((newNode) => customFunc(newNode, position === 'inside' ? node : parent));
|
||||
} else {
|
||||
customFunc(newNodes, parent || node.parent);
|
||||
customFunc(newNodes, position === 'inside' ? node : parent);
|
||||
}
|
||||
}
|
||||
// 插入后返回 true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
interface Tree {
|
||||
id: string | number;
|
||||
id?: string | number;
|
||||
groupId?: number;
|
||||
children?: Tree[];
|
||||
[key: string]: any;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<a-select
|
||||
v-model:model-value="method"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
:disabled="props.disabled"
|
||||
@change="(val) => emit('change', val as string)"
|
||||
>
|
||||
<template #label="{ data }">
|
||||
|
@ -24,6 +25,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
|
|
|
@ -834,6 +834,7 @@
|
|||
const formData = tempForm || requestVModel.value;
|
||||
if (fApi.value) {
|
||||
fApi.value.nextTick(() => {
|
||||
// 这里使用nextTick是因为插件表单使用v-if动态渲染,所以每次切换到插件表单时都会重新渲染插件表单并触发
|
||||
const form = {};
|
||||
controlPluginFormFields().forEach((key) => {
|
||||
form[key] = formData[key];
|
||||
|
@ -934,6 +935,17 @@
|
|||
const splitContainerRef = ref<HTMLElement>();
|
||||
const secondBoxHeight = ref(0);
|
||||
|
||||
watch(
|
||||
() => showResponse.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
splitBoxSize.value = 0.6;
|
||||
} else {
|
||||
splitBoxSize.value = 1;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => splitBoxSize.value,
|
||||
debounce((val) => {
|
||||
|
|
|
@ -1,56 +1,42 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="visible"
|
||||
:title="t('apiScenario.customApi')"
|
||||
:width="960"
|
||||
no-content-padding
|
||||
disabled-width-drag
|
||||
:show-continue="true"
|
||||
:footer="!!requestVModel.isNew"
|
||||
@confirm="handleSave"
|
||||
@continue="handleContinue"
|
||||
@close="handleClose"
|
||||
>
|
||||
<a-empty
|
||||
v-if="pluginError && !isHttpProtocol"
|
||||
:description="t('apiTestDebug.noPlugin')"
|
||||
class="h-[200px] items-center justify-center"
|
||||
>
|
||||
<template #image>
|
||||
<MsIcon type="icon-icon_plugin_outlined" size="48" />
|
||||
</template>
|
||||
</a-empty>
|
||||
<template #title>
|
||||
<div style="width: 100%">
|
||||
<div style="float: left"> {{ t('apiScenario.customApi') }}</div>
|
||||
<div style="float: right">
|
||||
<span
|
||||
v-show="requestVModel.useEnv === 'false'"
|
||||
style="
|
||||
float: left;
|
||||
margin-top: 8px;
|
||||
margin-right: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
"
|
||||
>{{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
|
||||
</span>
|
||||
|
||||
<MsSelect
|
||||
v-model:model-value="requestVModel.useEnv"
|
||||
:style="{ width: '150px', float: 'right' }"
|
||||
:allow-search="false"
|
||||
:options="[
|
||||
{ label: t('common.quote'), value: 'true' },
|
||||
{ label: t('common.notQuote'), value: 'false' },
|
||||
]"
|
||||
:multiple="false"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
:prefix="t('project.environmental.env')"
|
||||
@change="handleUseEnvChange"
|
||||
>
|
||||
</MsSelect>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<stepType
|
||||
v-if="props.requestType"
|
||||
v-show="props.requestType !== ScenarioStepType.CUSTOM_API"
|
||||
:type="props.requestType"
|
||||
/>
|
||||
{{ title }}
|
||||
</div>
|
||||
<div v-if="requestVModel.isNew" class="ml-auto flex items-center gap-[16px]">
|
||||
<div v-show="requestVModel.useEnv === 'false'" class="text-[14px] font-normal text-[var(--color-text-4)]">
|
||||
{{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
|
||||
</div>
|
||||
<MsSelect
|
||||
v-model:model-value="requestVModel.useEnv"
|
||||
:allow-search="false"
|
||||
:options="[
|
||||
{ label: t('common.quote'), value: 'true' },
|
||||
{ label: t('common.notQuote'), value: 'false' },
|
||||
]"
|
||||
:multiple="false"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
:prefix="t('project.environmental.env')"
|
||||
class="w-[150px]"
|
||||
@change="handleUseEnvChange"
|
||||
>
|
||||
</MsSelect>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
|
||||
|
@ -62,6 +48,7 @@
|
|||
v-model:model-value="requestVModel.protocol"
|
||||
:options="protocolOptions"
|
||||
:loading="protocolLoading"
|
||||
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
|
||||
class="w-[90px]"
|
||||
@change="(val) => handleActiveDebugProtocolChange(val as string)"
|
||||
/>
|
||||
|
@ -73,10 +60,7 @@
|
|||
is-tag
|
||||
class="flex items-center"
|
||||
/>
|
||||
<a-tooltip v-if="!isHttpProtocol" content="requestVModel.label" :mouse-enter-delay="500">
|
||||
<div class="one-line-text max-w-[350px]"> requestVModel.label</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="requestVModel.label" :mouse-enter-delay="500">
|
||||
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.label" :mouse-enter-delay="500">
|
||||
<div class="one-line-text max-w-[350px]"> {{ requestVModel.label }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
@ -84,6 +68,7 @@
|
|||
<apiMethodSelect
|
||||
v-model:model-value="requestVModel.method"
|
||||
class="w-[140px]"
|
||||
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<a-input
|
||||
|
@ -93,6 +78,7 @@
|
|||
allow-clear
|
||||
class="hover:z-10"
|
||||
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
|
||||
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
|
||||
@input="() => (isUrlError = false)"
|
||||
@change="handleUrlChange"
|
||||
/>
|
||||
|
@ -100,25 +86,41 @@
|
|||
</div>
|
||||
<div>
|
||||
<a-dropdown-button
|
||||
v-if="!requestVModel.executeLoading"
|
||||
v-if="hasLocalExec"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
@select="execute"
|
||||
>
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
|
||||
<template v-if="hasLocalExec" #icon>
|
||||
<template #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template v-if="hasLocalExec" #content>
|
||||
<template #content>
|
||||
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
|
||||
{{ t('apiTestDebug.serverExec') }}
|
||||
</a-button>
|
||||
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-input
|
||||
v-if="
|
||||
props.requestType &&
|
||||
[ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(props.requestType) &&
|
||||
isHttpProtocol
|
||||
"
|
||||
v-model:model-value="requestVModel.name"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
|
||||
disabled
|
||||
allow-clear
|
||||
class="mt-[8px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-[16px]">
|
||||
<MsTab
|
||||
|
@ -234,6 +236,7 @@
|
|||
</template>
|
||||
<template #second>
|
||||
<response
|
||||
v-if="visible"
|
||||
v-show="showResponse"
|
||||
v-model:active-layout="activeLayout"
|
||||
v-model:active-tab="requestVModel.responseActiveTab"
|
||||
|
@ -255,7 +258,16 @@
|
|||
</MsSplitBox>
|
||||
</div>
|
||||
</div>
|
||||
<addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" />
|
||||
<a-empty
|
||||
v-if="pluginError && !isHttpProtocol"
|
||||
:description="t('apiTestDebug.noPlugin')"
|
||||
class="h-[200px] items-center justify-center"
|
||||
>
|
||||
<template #image>
|
||||
<MsIcon type="icon-icon_plugin_outlined" size="48" />
|
||||
</template>
|
||||
</a-empty>
|
||||
<!-- <addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" /> -->
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
|
@ -271,6 +283,7 @@
|
|||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import assertion from '@/components/business/ms-assertion/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import stepType from './stepType.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||
import auth from '@/views/api-test/components/requestComposition/auth.vue';
|
||||
|
@ -280,8 +293,8 @@
|
|||
import setting from '@/views/api-test/components/requestComposition/setting.vue';
|
||||
|
||||
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
|
||||
import { getDefinitionDetail } from '@/api/modules/api-test/management';
|
||||
import { getSocket } from '@/api/modules/project-management/commonScript';
|
||||
import { getLocalConfig } from '@/api/modules/user/index';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useAppStore } from '@/store';
|
||||
import { getGenerateId, parseQueryParams } from '@/utils';
|
||||
|
@ -294,7 +307,6 @@
|
|||
PluginConfig,
|
||||
RequestTaskResult,
|
||||
} from '@/models/apiTest/common';
|
||||
import { CustomApiStep } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||
import {
|
||||
RequestAuthType,
|
||||
|
@ -303,6 +315,7 @@
|
|||
RequestConditionProcessor,
|
||||
RequestMethods,
|
||||
ResponseComposition,
|
||||
ScenarioStepType,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
import {
|
||||
|
@ -316,23 +329,20 @@
|
|||
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
import type { Api } from '@form-create/arco-design';
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
|
||||
// 懒加载Http协议组件
|
||||
const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue'));
|
||||
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
|
||||
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
|
||||
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
|
||||
const addDependencyDrawer = defineAsyncComponent(
|
||||
() => import('@/views/api-test/management/components/addDependencyDrawer.vue')
|
||||
);
|
||||
// const addDependencyDrawer = defineAsyncComponent(
|
||||
// () => import('@/views/api-test/management/components/addDependencyDrawer.vue')
|
||||
// );
|
||||
|
||||
export interface RequestCustomAttr {
|
||||
type: 'api';
|
||||
isNew: boolean;
|
||||
protocol: string;
|
||||
activeTab: RequestComposition;
|
||||
mode?: 'debug';
|
||||
executeLoading: boolean; // 执行中loading
|
||||
isCopy?: boolean; // 是否是复制
|
||||
isExecute?: boolean; // 是否是执行
|
||||
|
@ -341,11 +351,14 @@
|
|||
export type RequestParam = ExecuteApiRequestFullParams & {
|
||||
response?: RequestTaskResult;
|
||||
useEnv: string;
|
||||
request?: ExecuteApiRequestFullParams; // 请求参数集合
|
||||
} & RequestCustomAttr &
|
||||
TabItem;
|
||||
|
||||
const props = defineProps<{
|
||||
request?: RequestParam; // 请求参数集合
|
||||
requestType?: ScenarioStepType;
|
||||
stepName: string;
|
||||
detailLoading?: boolean; // 详情加载状态
|
||||
envDetailItem?: {
|
||||
id?: string;
|
||||
|
@ -367,11 +380,13 @@
|
|||
|
||||
const emit = defineEmits<{
|
||||
(e: 'addStep', request: RequestParam): void;
|
||||
(e: 'applyStep', request: RequestParam): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
const loading = defineModel<boolean>('detailLoading', { default: false });
|
||||
|
||||
const defaultDebugParams: RequestParam = {
|
||||
|
@ -438,7 +453,12 @@
|
|||
};
|
||||
|
||||
const requestVModel = ref<RequestParam>(props.request || defaultDebugParams);
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.requestType && [ScenarioStepType.COPY_API, ScenarioStepType.QUOTE_API].includes(props.requestType)) {
|
||||
return props.stepName;
|
||||
}
|
||||
return t('apiScenario.customApi');
|
||||
});
|
||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
const isInitPluginForm = ref(false);
|
||||
|
@ -506,9 +526,7 @@
|
|||
const contentTabList = computed(() => {
|
||||
// HTTP 协议 tabs
|
||||
if (isHttpProtocol.value) {
|
||||
return requestVModel.value.mode === 'debug'
|
||||
? httpContentTabList
|
||||
: httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
|
||||
return httpContentTabList;
|
||||
}
|
||||
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
|
||||
});
|
||||
|
@ -568,7 +586,7 @@
|
|||
const localExecuteUrl = ref('');
|
||||
|
||||
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); // 存储初始化过后的插件配置
|
||||
const temporaryPluginFormMap: Record<string, any> = {}; // 缓存插件表单,避免切换tab导致动态表单数据丢失
|
||||
const temporaryPluginFormMap: Record<string, any> = {}; // 缓存插件表单,避免切换传入的 API 数据导致动态表单数据丢失
|
||||
const pluginLoading = ref(false);
|
||||
const fApi = ref<Api>();
|
||||
const currentPluginOptions = computed<Record<string, any>>(
|
||||
|
@ -577,10 +595,19 @@
|
|||
const currentPluginScript = computed<Record<string, any>[]>(
|
||||
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
|
||||
);
|
||||
const isCopyApiNeedInit = computed(
|
||||
() => props.requestType === ScenarioStepType.COPY_API && props.request?.request === null
|
||||
);
|
||||
const isEditableApi = computed(
|
||||
() => props.requestType === ScenarioStepType.COPY_API || props.requestType === ScenarioStepType.CUSTOM_API
|
||||
);
|
||||
|
||||
// 处理插件表单输入框变化
|
||||
const handlePluginFormChange = debounce(() => {
|
||||
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData();
|
||||
if (isEditableApi.value) {
|
||||
// 复制或者新建的时候需要缓存表单数据,引用的不能更改
|
||||
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData();
|
||||
}
|
||||
handleActiveDebugChange();
|
||||
}, 300);
|
||||
|
||||
|
@ -604,11 +631,11 @@
|
|||
*/
|
||||
function setPluginFormData() {
|
||||
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
|
||||
if (tempForm || !requestVModel.value.isNew || requestVModel.value.isCopy) {
|
||||
if (tempForm || !requestVModel.value.isNew) {
|
||||
// 如果缓存的表单数据存在或者是编辑状态,则需要将之前的输入数据填充
|
||||
const formData = tempForm || requestVModel.value;
|
||||
const formData = isEditableApi.value ? tempForm || requestVModel.value : requestVModel.value;
|
||||
if (fApi.value) {
|
||||
fApi.value.nextTick(() => {
|
||||
fApi.value.nextRefresh(() => {
|
||||
const form = {};
|
||||
controlPluginFormFields().forEach((key) => {
|
||||
form[key] = formData[key];
|
||||
|
@ -621,12 +648,8 @@
|
|||
});
|
||||
}
|
||||
} else {
|
||||
fApi.value?.nextTick(() => {
|
||||
controlPluginFormFields();
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是没有缓存也不是编辑,则需要重置表单,因为 form-create 只有一个实例,已经被其他有数据的 tab 污染了,需要重置
|
||||
fApi.value?.resetFields();
|
||||
controlPluginFormFields();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -720,6 +743,17 @@
|
|||
const splitContainerRef = ref<HTMLElement>();
|
||||
const secondBoxHeight = ref(0);
|
||||
|
||||
watch(
|
||||
() => showResponse.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
splitBoxSize.value = 0.6;
|
||||
} else {
|
||||
splitBoxSize.value = 1;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => splitBoxSize.value,
|
||||
debounce((val) => {
|
||||
|
@ -927,6 +961,7 @@
|
|||
await props.localExecuteApi(localExecuteUrl.value, res);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
requestVModel.value.executeLoading = false;
|
||||
|
@ -964,8 +999,8 @@
|
|||
}
|
||||
|
||||
function handleContinue() {
|
||||
requestVModel.value.isNew = false; // 添加完就不是新建了
|
||||
emit('addStep', requestVModel.value);
|
||||
requestVModel.value = { ...defaultDebugParams };
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
|
@ -973,36 +1008,73 @@
|
|||
visible.value = false;
|
||||
}
|
||||
|
||||
const isUrlError = ref(false);
|
||||
const showAddDependencyDrawer = ref(false);
|
||||
const addDependencyMode = ref<'pre' | 'post'>('pre');
|
||||
|
||||
// watch(
|
||||
// () => visible.value,
|
||||
// async (val) => {
|
||||
// if (val) {
|
||||
// await initProtocolList();
|
||||
// if (props.request) {
|
||||
// requestVModel.value = { ...defaultDebugParams, ...props.request };
|
||||
// handleActiveDebugProtocolChange(requestVModel.value.protocol);
|
||||
// } else {
|
||||
// requestVModel.value = { ...defaultDebugParams };
|
||||
// }
|
||||
// } else {
|
||||
// requestVModel.value = { ...defaultDebugParams };
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
onBeforeMount(() => {
|
||||
initProtocolList();
|
||||
if (props.request) {
|
||||
requestVModel.value = { ...defaultDebugParams, ...props.request };
|
||||
handleActiveDebugProtocolChange(requestVModel.value.protocol);
|
||||
} else {
|
||||
requestVModel.value = { ...defaultDebugParams };
|
||||
function handleClose() {
|
||||
// 关闭时若不是创建行为则是编辑行为,需要触发 applyStep
|
||||
if (!requestVModel.value.isNew) {
|
||||
emit('applyStep', requestVModel.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isUrlError = ref(false);
|
||||
// const showAddDependencyDrawer = ref(false);
|
||||
// const addDependencyMode = ref<'pre' | 'post'>('pre');
|
||||
|
||||
async function initQuoteApiDetail() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getDefinitionDetail(requestVModel.value.id);
|
||||
let parseRequestBodyResult;
|
||||
if (res.protocol === 'HTTP') {
|
||||
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
}
|
||||
requestVModel.value = {
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
executeLoading: false,
|
||||
activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
|
||||
unSaved: false,
|
||||
isNew: false,
|
||||
label: res.name,
|
||||
...res.request,
|
||||
...res,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
|
||||
url: res.path,
|
||||
name: res.name, // request里面还有个name但是是null
|
||||
id: res.id,
|
||||
...parseRequestBodyResult,
|
||||
};
|
||||
nextTick(() => {
|
||||
// 等待内容渲染出来再隐藏loading
|
||||
loading.value = false;
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
if (props.request) {
|
||||
requestVModel.value = { ...defaultDebugParams, ...props.request };
|
||||
if (
|
||||
props.requestType === ScenarioStepType.QUOTE_API ||
|
||||
isCopyApiNeedInit.value
|
||||
// 引用接口时,需要初始化引用接口的详情;复制只在第一次初始化的时候需要加载后台数据(request.request是复制请求时列表参数字段request会为 null,以此判断释放第一次初始化)
|
||||
) {
|
||||
initQuoteApiDetail();
|
||||
}
|
||||
}
|
||||
await initProtocolList();
|
||||
if (props.request) {
|
||||
handleActiveDebugProtocolChange(requestVModel.value.protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -233,9 +233,10 @@
|
|||
*/
|
||||
function handleSelectAllChange(v: SelectAllEnum) {
|
||||
if (v === SelectAllEnum.CURRENT) {
|
||||
tableSelectedData.value = currentTable.value.propsRes.value.data;
|
||||
tableSelectedData.value.push(...currentTable.value.propsRes.value.data);
|
||||
} else {
|
||||
tableSelectedData.value = [];
|
||||
const dataSet = new Set(currentTable.value.propsRes.value.data.map((e) => e.id));
|
||||
tableSelectedData.value = tableSelectedData.value.filter((e) => !dataSet.has(e.id));
|
||||
}
|
||||
emit('select', tableSelectedData.value);
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.LOOP_CONTROL,
|
||||
name: t('apiScenario.loopControl'),
|
||||
|
@ -134,7 +134,7 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.CONDITION_CONTROL,
|
||||
name: t('apiScenario.conditionControl'),
|
||||
|
@ -156,7 +156,7 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.ONLY_ONCE_CONTROL,
|
||||
name: t('apiScenario.onlyOnceControl'),
|
||||
|
@ -178,7 +178,7 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: getGenerateId(),
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.WAIT_TIME,
|
||||
name: t('apiScenario.waitTime'),
|
||||
|
@ -189,7 +189,7 @@
|
|||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
case ScenarioAddStepActionType.SCRIPT_OPERATION:
|
||||
if (step.value) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
emit('otherCreate', val, realStep as ScenarioStepItem);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
position="br"
|
||||
@popup-visible-change="handleActionTriggerChange"
|
||||
>
|
||||
<MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
|
||||
<MsButton :id="step.stepId" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
<template #content>
|
||||
|
@ -131,7 +131,7 @@
|
|||
function handleActionsClose() {
|
||||
activeCreateAction.value = undefined;
|
||||
innerStep.value.createActionsVisible = false;
|
||||
document.getElementById(innerStep.value.id.toString())?.click();
|
||||
document.getElementById(innerStep.value.stepId.toString())?.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -15,18 +15,18 @@ export default function useCreateActions() {
|
|||
|
||||
/**
|
||||
* 插入步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||
* @param selectedKeys 选中的步骤 id 集合
|
||||
* @param step 需要判断的步骤
|
||||
* @param selectedKeys 选中的步骤 stepId 集合
|
||||
* @param steps 需要判断的步骤
|
||||
* @param parent 需要判断的父节点
|
||||
*/
|
||||
function checkedIfNeed(
|
||||
selectedKeys: (string | number)[],
|
||||
step: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
|
||||
steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
|
||||
parent?: TreeNode<ScenarioStepItem>
|
||||
) {
|
||||
if (parent && selectedKeys.includes(parent.id)) {
|
||||
if (parent && selectedKeys.includes(parent.stepId)) {
|
||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys = selectedKeys.concat(step.map((item) => item.id));
|
||||
selectedKeys.push(...steps.map((item) => item.stepId));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ export default function useCreateActions() {
|
|||
* @param step 目标步骤
|
||||
* @param steps 顶层步骤列表
|
||||
* @param createStepAction 创建步骤操作类型
|
||||
* @param selectedKeys 选中的步骤 id 集合
|
||||
* @param selectedKeys 选中的步骤 stepId 集合
|
||||
*/
|
||||
function handleCreateStep(
|
||||
defaultStepInfo: ScenarioStepItem,
|
||||
|
@ -48,7 +48,7 @@ export default function useCreateActions() {
|
|||
const newStep = {
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...defaultStepInfo,
|
||||
id: getGenerateId(),
|
||||
stepId: getGenerateId(),
|
||||
};
|
||||
switch (createStepAction) {
|
||||
case 'inside':
|
||||
|
@ -64,11 +64,11 @@ export default function useCreateActions() {
|
|||
}
|
||||
insertNodes<ScenarioStepItem>(
|
||||
step.parent?.children || steps,
|
||||
step.id,
|
||||
step.stepId,
|
||||
newStep,
|
||||
createStepAction,
|
||||
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
|
||||
'id'
|
||||
'stepId'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,8 @@ export default function useCreateActions() {
|
|||
function buildInsertStepInfos(
|
||||
newSteps: Record<string, any>[],
|
||||
type: ScenarioStepType,
|
||||
startOrder: number
|
||||
startOrder: number,
|
||||
stepsDetailMap: Record<string, any>
|
||||
): ScenarioStepItem[] {
|
||||
let name: string;
|
||||
switch (type) {
|
||||
|
@ -125,10 +126,12 @@ export default function useCreateActions() {
|
|||
break;
|
||||
}
|
||||
return newSteps.map((item, index) => {
|
||||
const stepId = getGenerateId();
|
||||
stepsDetailMap[stepId] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息
|
||||
return {
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
...item,
|
||||
id: getGenerateId(),
|
||||
stepId,
|
||||
type,
|
||||
name,
|
||||
order: startOrder + index,
|
||||
|
@ -143,7 +146,7 @@ export default function useCreateActions() {
|
|||
* @param steps 顶层步骤列表
|
||||
* @param createStepAction 创建步骤操作类型
|
||||
* @param type 需要插入的步骤类型
|
||||
* @param selectedKeys 选中的步骤 id 集合
|
||||
* @param selectedKeys 选中的步骤 stepId 集合
|
||||
*/
|
||||
function handleCreateSteps(
|
||||
step: ScenarioStepItem,
|
||||
|
@ -154,11 +157,11 @@ export default function useCreateActions() {
|
|||
) {
|
||||
insertNodes<ScenarioStepItem>(
|
||||
step.parent?.children || steps,
|
||||
step.id,
|
||||
step.stepId,
|
||||
readyInsertSteps,
|
||||
createStepAction,
|
||||
undefined,
|
||||
'id'
|
||||
'stepId'
|
||||
);
|
||||
checkedIfNeed(selectedKeys, readyInsertSteps, step);
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
v-model:checked-keys="checkedKeys"
|
||||
v-model:stepKeyword="keyword"
|
||||
:expand-all="isExpandAll"
|
||||
:steps-detail-map="stepInfo.stepsDetailMap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -127,6 +128,7 @@
|
|||
executeTime?: string; // 执行时间
|
||||
executeSuccessCount?: number; // 执行成功数量
|
||||
executeFailCount?: number; // 执行失败数量
|
||||
stepsDetailMap: Record<string, any>; // 步骤详情存储
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -200,7 +202,7 @@
|
|||
try {
|
||||
let ids = checkedKeys.value;
|
||||
if (batchToggleRange.value === 'top') {
|
||||
ids = stepInfo.value.steps.map((item) => item.id);
|
||||
ids = stepInfo.value.steps.map((item) => item.stepId);
|
||||
}
|
||||
console.log('ids', ids);
|
||||
await new Promise((resolve) => {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</a-select>
|
||||
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
:id="innerData.stepId"
|
||||
v-model:model-value="innerData.variableVal"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
|
@ -41,7 +41,7 @@
|
|||
import { conditionOptions } from '@/views/api-test/scenario/components/config';
|
||||
|
||||
export interface ConditionContentProps {
|
||||
id: string;
|
||||
stepId: string;
|
||||
variableName: string;
|
||||
condition: string;
|
||||
variableVal: string;
|
||||
|
@ -75,7 +75,7 @@
|
|||
() => dbClick?.value.timeStamp,
|
||||
() => {
|
||||
// @ts-ignore
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
|
||||
emit('quickInput', 'variableVal');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
</a-select>
|
||||
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
:id="innerData.stepId"
|
||||
v-model:model-value="innerData.variableVal"
|
||||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
|
@ -99,7 +99,7 @@
|
|||
</template>
|
||||
<a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression">
|
||||
<a-input
|
||||
:id="innerData.id"
|
||||
:id="innerData.stepId"
|
||||
v-model:model-value="innerData.expression"
|
||||
size="mini"
|
||||
class="w-[200px] px-[8px]"
|
||||
|
@ -159,7 +159,7 @@
|
|||
import { conditionOptions } from '@/views/api-test/scenario/components/config';
|
||||
|
||||
export interface LoopContentProps {
|
||||
id: string | number;
|
||||
stepId: string | number;
|
||||
num: number;
|
||||
name: string;
|
||||
type: ScenarioStepType;
|
||||
|
@ -227,7 +227,7 @@
|
|||
() => dbClick?.value.timeStamp,
|
||||
() => {
|
||||
// @ts-ignore
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
|
||||
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
|
||||
emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
const props = defineProps<{
|
||||
data: {
|
||||
id: string | number;
|
||||
stepId: string | number;
|
||||
belongProjectId: string;
|
||||
belongProjectName: string;
|
||||
num: number;
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
export interface WaitTimeContentProps {
|
||||
id: string | number;
|
||||
stepId: string | number;
|
||||
waitTime: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
:expand-all="props.expandAll"
|
||||
:node-more-actions="stepMoreActions"
|
||||
:filter-more-action-func="setStepMoreAction"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:field-names="{ title: 'name', key: 'stepId', children: 'children' }"
|
||||
:virtual-list-props="{
|
||||
height: '100%',
|
||||
threshold: 20,
|
||||
|
@ -88,7 +88,7 @@
|
|||
<template v-if="checkStepIsApi(step)">
|
||||
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
|
||||
<div
|
||||
v-if="step.id === showStepNameEditInputStepId"
|
||||
v-if="step.stepId === showStepNameEditInputStepId"
|
||||
class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<!-- 其他步骤描述 -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="step.id === showStepDescEditInputStepId"
|
||||
v-if="step.stepId === showStepDescEditInputStepId"
|
||||
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
|
||||
>
|
||||
<a-input
|
||||
|
@ -161,7 +161,7 @@
|
|||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="steps"
|
||||
:step="step"
|
||||
@click="setFocusNodeKey(step.id)"
|
||||
@click="setFocusNodeKey(step.stepId)"
|
||||
@other-create="handleOtherCreate"
|
||||
@close="setFocusNodeKey('')"
|
||||
/>
|
||||
|
@ -187,11 +187,13 @@
|
|||
</a-button>
|
||||
</createStepActions>
|
||||
<customApiDrawer
|
||||
v-if="customApiDrawerVisible"
|
||||
v-model:visible="customApiDrawerVisible"
|
||||
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
|
||||
:request="activeStep?.request"
|
||||
:request="currentStepDetail"
|
||||
:request-type="activeStep?.type"
|
||||
:step-name="activeStep?.name || ''"
|
||||
@add-step="addCustomApiStep"
|
||||
@apply-step="applyApiStep"
|
||||
/>
|
||||
<importApiDrawer
|
||||
v-if="importApiDrawerVisible"
|
||||
|
@ -202,7 +204,7 @@
|
|||
<scriptOperationDrawer
|
||||
v-if="scriptOperationDrawerVisible"
|
||||
v-model:visible="scriptOperationDrawerVisible"
|
||||
:script="activeStep?.script"
|
||||
:script="currentStepDetail"
|
||||
:name="activeStep?.name"
|
||||
@save="addScriptStep"
|
||||
/>
|
||||
|
@ -285,7 +287,7 @@
|
|||
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
|
||||
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
stepId: string | number;
|
||||
order: number;
|
||||
enabled: boolean; // 是否启用
|
||||
type: ScenarioStepType;
|
||||
|
@ -333,6 +335,10 @@
|
|||
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
|
||||
required: true,
|
||||
});
|
||||
// 步骤详情映射,存储部分抽屉展示详情的数据
|
||||
const stepsDetailMap = defineModel<Record<string, any>>('stepsDetailMap', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const loading = ref(false);
|
||||
|
@ -391,9 +397,9 @@
|
|||
* 增加步骤时判断父节点是否选中,如果选中则需要把新节点也选中
|
||||
*/
|
||||
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
|
||||
if (parent && selectedKeys.value.includes(parent.id)) {
|
||||
if (parent && selectedKeys.value.includes(parent.stepId)) {
|
||||
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
selectedKeys.value.push(step.id);
|
||||
selectedKeys.value.push(step.stepId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -452,7 +458,7 @@
|
|||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
label: 'apiScenario.saveAsCase',
|
||||
label: 'apiTestManagement.saveAsCase',
|
||||
eventTag: 'saveAsCase',
|
||||
},
|
||||
{
|
||||
|
@ -471,30 +477,30 @@
|
|||
const id = getGenerateId();
|
||||
insertNodes<ScenarioStepItem>(
|
||||
steps.value,
|
||||
node.id,
|
||||
node.stepId,
|
||||
{
|
||||
...cloneDeep(
|
||||
mapTree<ScenarioStepItem>(node, (childNode) => {
|
||||
return {
|
||||
...childNode,
|
||||
id: getGenerateId(), // TODO:引用类型额外需要一个复制来源 ID
|
||||
stepId: getGenerateId(), // TODO:引用类型额外需要一个复制来源 ID
|
||||
};
|
||||
})[0]
|
||||
),
|
||||
name: `copy-${node.name}`,
|
||||
order: node.order + 1,
|
||||
id,
|
||||
stepId: id,
|
||||
},
|
||||
'after',
|
||||
checkedIfNeed,
|
||||
'id'
|
||||
'stepId'
|
||||
);
|
||||
break;
|
||||
case 'config':
|
||||
console.log('config', node);
|
||||
break;
|
||||
case 'delete':
|
||||
deleteNode(steps.value, node.id, 'id');
|
||||
deleteNode(steps.value, node.stepId, 'stepId');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -512,7 +518,7 @@
|
|||
const tempStepName = ref('');
|
||||
function handleStepNameClick(step: ScenarioStepItem) {
|
||||
tempStepName.value = step.name;
|
||||
showStepNameEditInputStepId.value = step.id;
|
||||
showStepNameEditInputStepId.value = step.stepId;
|
||||
nextTick(() => {
|
||||
// 等待输入框渲染完成后聚焦
|
||||
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||
|
@ -521,7 +527,7 @@
|
|||
}
|
||||
|
||||
function applyStepNameChange(step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
realStep.name = tempStepName.value;
|
||||
}
|
||||
|
@ -535,7 +541,7 @@
|
|||
const tempStepDesc = ref('');
|
||||
function handleStepDescClick(step: ScenarioStepItem) {
|
||||
tempStepDesc.value = step.description;
|
||||
showStepDescEditInputStepId.value = step.id;
|
||||
showStepDescEditInputStepId.value = step.stepId;
|
||||
nextTick(() => {
|
||||
// 等待输入框渲染完成后聚焦
|
||||
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||
|
@ -544,7 +550,7 @@
|
|||
}
|
||||
|
||||
function applyStepDescChange(step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
realStep.description = tempStepDesc.value;
|
||||
}
|
||||
|
@ -552,7 +558,7 @@
|
|||
}
|
||||
|
||||
function handleStepContentChange($event, step: ScenarioStepItem) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
Object.keys($event).forEach((key) => {
|
||||
realStep[key] = $event[key];
|
||||
|
@ -564,7 +570,7 @@
|
|||
* 处理步骤展开折叠
|
||||
*/
|
||||
function handleStepExpand(data: MsTreeExpandedData) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
realStep.expanded = !realStep.expanded;
|
||||
}
|
||||
|
@ -575,15 +581,27 @@
|
|||
const scriptOperationDrawerVisible = ref(false);
|
||||
const activeStep = ref<ScenarioStepItem>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
||||
const activeCreateAction = ref<CreateStepAction>(); // 用于抽屉操作创建步骤时记录当前插入类型
|
||||
const currentStepDetail = computed<any>(() => {
|
||||
// TODO: 步骤详情类型
|
||||
if (activeStep.value) {
|
||||
return stepsDetailMap.value[activeStep.value.stepId];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理步骤选中事件
|
||||
* @param _selectedKeys 选中的 key集合
|
||||
* @param step 点击的步骤节点
|
||||
*/
|
||||
function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(step.children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
offspringIds.push(e.stepId);
|
||||
return e;
|
||||
});
|
||||
selectedKeys.value = [step.id, ...offspringIds];
|
||||
if (step.type === ScenarioStepType.CUSTOM_API) {
|
||||
selectedKeys.value = [step.stepId, ...offspringIds];
|
||||
if ([ScenarioStepType.CUSTOM_API, ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(step.type)) {
|
||||
activeStep.value = step;
|
||||
customApiDrawerVisible.value = true;
|
||||
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
|
||||
|
@ -651,17 +669,20 @@
|
|||
const insertApiSteps = buildInsertStepInfos(
|
||||
data.api,
|
||||
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API,
|
||||
order
|
||||
order,
|
||||
stepsDetailMap.value
|
||||
);
|
||||
const insertCaseSteps = buildInsertStepInfos(
|
||||
data.case,
|
||||
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE,
|
||||
order + insertApiSteps.length
|
||||
order + insertApiSteps.length,
|
||||
stepsDetailMap.value
|
||||
);
|
||||
const insertScenarioSteps = buildInsertStepInfos(
|
||||
data.scenario,
|
||||
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO,
|
||||
order + insertApiSteps.length + insertCaseSteps.length
|
||||
order + insertApiSteps.length + insertCaseSteps.length,
|
||||
stepsDetailMap.value
|
||||
);
|
||||
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
|
@ -675,12 +696,13 @@
|
|||
* 添加自定义 API 步骤
|
||||
*/
|
||||
function addCustomApiStep(request: RequestParam) {
|
||||
const id = getGenerateId();
|
||||
stepsDetailMap.value[id] = request;
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
handleCreateStep(
|
||||
{
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: t('apiScenario.customApi'),
|
||||
request: cloneDeep(request),
|
||||
} as ScenarioStepItem,
|
||||
activeStep.value,
|
||||
steps.value,
|
||||
|
@ -690,25 +712,35 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: id,
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.CUSTOM_API,
|
||||
name: t('apiScenario.customApi'),
|
||||
request: cloneDeep(request),
|
||||
} as ScenarioStepItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 详情抽屉关闭时应用更改
|
||||
*/
|
||||
function applyApiStep(request: RequestParam) {
|
||||
if (activeStep.value) {
|
||||
stepsDetailMap.value[activeStep.value?.stepId] = request;
|
||||
activeStep.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加脚本操作步骤
|
||||
*/
|
||||
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
|
||||
const id = getGenerateId();
|
||||
stepsDetailMap.value[id] = cloneDeep(scriptProcessor);
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
handleCreateStep(
|
||||
{
|
||||
type: ScenarioStepType.SCRIPT_OPERATION,
|
||||
name,
|
||||
script: cloneDeep(scriptProcessor),
|
||||
} as ScenarioStepItem,
|
||||
activeStep.value,
|
||||
steps.value,
|
||||
|
@ -718,11 +750,10 @@
|
|||
} else {
|
||||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id: getGenerateId(),
|
||||
stepId: id,
|
||||
order: steps.value.length + 1,
|
||||
type: ScenarioStepType.SCRIPT_OPERATION,
|
||||
name,
|
||||
script: cloneDeep(scriptProcessor),
|
||||
} as ScenarioStepItem);
|
||||
}
|
||||
}
|
||||
|
@ -760,20 +791,20 @@
|
|||
loading.value = true;
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(dragNode.children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
offspringIds.push(e.stepId);
|
||||
return e;
|
||||
});
|
||||
const stepIdAndOffspringIds = [dragNode.id, ...offspringIds];
|
||||
const stepIdAndOffspringIds = [dragNode.stepId, ...offspringIds];
|
||||
if (dropPosition === 0) {
|
||||
// 拖拽到节点内
|
||||
if (selectedKeys.value.includes(dropNode.id)) {
|
||||
if (selectedKeys.value.includes(dropNode.stepId)) {
|
||||
// 释放位置的节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
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.stepId)) {
|
||||
// 释放位置的节点的父节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||
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.stepId)) {
|
||||
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点及其子孙节点从选中的节点中移除
|
||||
selectedKeys.value = selectedKeys.value.filter((e) => {
|
||||
for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
|
||||
|
@ -786,7 +817,7 @@
|
|||
return true;
|
||||
});
|
||||
}
|
||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
|
||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'stepId');
|
||||
if (dragResult) {
|
||||
Message.success(t('common.moveSuccess'));
|
||||
}
|
||||
|
@ -805,7 +836,7 @@
|
|||
const quickInputDataKey = ref('');
|
||||
|
||||
function setQuickInput(step: ScenarioStepItem, dataKey: string) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
|
||||
if (realStep) {
|
||||
activeStep.value = realStep as ScenarioStepItem;
|
||||
}
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
executeTime: '',
|
||||
executeSuccessCount: 0,
|
||||
executeFailCount: 0,
|
||||
stepsDetailMap: {},
|
||||
} as ScenarioStepInfo,
|
||||
status: RequestDefinitionStatus.PROCESSING,
|
||||
tags: [],
|
||||
|
|
Loading…
Reference in New Issue