feat(接口场景): 脚本操作&api、case 文件处理&交互优化
This commit is contained in:
parent
7048ec91e2
commit
56ad892491
|
@ -49,7 +49,7 @@
|
|||
"@tiptap/vue-3": "^2.1.13",
|
||||
"@types/color": "^3.0.4",
|
||||
"@types/node": "^20.11.16",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"ace-builds": "^1.24.2",
|
||||
"ahooks-vue": "^0.15.1",
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
await appStore.initSystemVersion(); // 初始化系统版本
|
||||
appStore.initSystemVersion(); // 初始化系统版本
|
||||
// 企业版才校验license
|
||||
if (appStore.packageType === 'enterprise') {
|
||||
licenseStore.getValidateLicense();
|
||||
|
@ -115,7 +115,7 @@
|
|||
setLocalStorage('salt', publicKey);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
await getPublicKey();
|
||||
if (WHITE_LIST.find((el) => el.path === window.location.hash.split('#')[1]) === undefined) {
|
||||
await checkIsLogin();
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
UpdateScenarioUrl,
|
||||
} from '@/api/requrls/api-test/scenario';
|
||||
|
||||
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||
import {
|
||||
ApiScenarioBatchDeleteParams,
|
||||
ApiScenarioBatchEditParams,
|
||||
|
@ -55,6 +56,9 @@ import {
|
|||
} from '@/models/apiTest/scenario';
|
||||
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common';
|
||||
|
||||
import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
|
||||
|
||||
// 更新模块
|
||||
export function updateModule(data: ApiScenarioModuleUpdateParams) {
|
||||
return MSR.post({ url: UpdateModuleUrl, data });
|
||||
|
@ -207,7 +211,10 @@ export function getScenarioDetail(id: string) {
|
|||
|
||||
// 获取场景步骤详情
|
||||
export function getScenarioStep(stepId: string | number) {
|
||||
return MSR.get({ url: GetScenarioStepUrl, params: stepId });
|
||||
return MSR.get<Partial<RequestParam & CaseRequestParam & ExecuteConditionProcessor>>({
|
||||
url: GetScenarioStepUrl,
|
||||
params: stepId,
|
||||
});
|
||||
}
|
||||
|
||||
// 文件转存
|
||||
|
|
|
@ -68,31 +68,31 @@
|
|||
arrow-class="hidden"
|
||||
:popup-offset="0"
|
||||
>
|
||||
<MsTagsInput
|
||||
v-model:model-value="inputFiles"
|
||||
:disabled="props.disabled"
|
||||
:input-class="props.inputClass"
|
||||
placeholder=" "
|
||||
:max-tag-count="1"
|
||||
:size="props.inputSize"
|
||||
readonly
|
||||
class="!w-[calc(100%-28px)]"
|
||||
>
|
||||
<template v-if="alreadyDeleteFiles.length > 0" #prefix>
|
||||
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
|
||||
</template>
|
||||
<template #tag="{ data }">
|
||||
<MsTag
|
||||
:size="props.tagSize"
|
||||
class="m-0 border-none p-0"
|
||||
:self-style="{ backgroundColor: 'transparent !important' }"
|
||||
:closable="data.value !== '__arco__more'"
|
||||
@close="handleClose(data)"
|
||||
>
|
||||
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
|
||||
</MsTag>
|
||||
</template>
|
||||
</MsTagsInput>
|
||||
<div class="!w-[calc(100%-28px)]">
|
||||
<MsTagsInput
|
||||
v-model:model-value="inputFiles"
|
||||
:input-class="props.inputClass"
|
||||
placeholder=" "
|
||||
:max-tag-count="1"
|
||||
:size="props.inputSize"
|
||||
readonly
|
||||
>
|
||||
<template v-if="alreadyDeleteFiles.length > 0" #prefix>
|
||||
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
|
||||
</template>
|
||||
<template #tag="{ data }">
|
||||
<MsTag
|
||||
:size="props.tagSize"
|
||||
class="m-0 border-none p-0"
|
||||
:self-style="{ backgroundColor: 'transparent !important' }"
|
||||
:closable="data.value !== '__arco__more'"
|
||||
@close="handleClose(data)"
|
||||
>
|
||||
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
|
||||
</MsTag>
|
||||
</template>
|
||||
</MsTagsInput>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex w-[200px] flex-col gap-[8px]">
|
||||
<template v-if="alreadyDeleteFiles.length > 0">
|
||||
|
@ -379,6 +379,14 @@
|
|||
|
||||
function handleSaveFileFinish(fileId: string) {
|
||||
if (savingFile.value) {
|
||||
inputFiles.value = inputFiles.value.map((e) => {
|
||||
if (e.value === savingFile.value?.fileId || e.value === savingFile.value?.uid) {
|
||||
// 被存储过的文件没有 uid,只有fileId;刚上传还未保存的文件只有 uid,没有fileId
|
||||
e.value = fileId;
|
||||
e.local = false;
|
||||
}
|
||||
return e;
|
||||
});
|
||||
savingFile.value.fileId = fileId;
|
||||
savingFile.value.local = false;
|
||||
}
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
<div class="relative w-full">
|
||||
<MsCodeEditor
|
||||
ref="codeEditorRef"
|
||||
v-model:model-value="innerCodeValue"
|
||||
v-model:model-value="code"
|
||||
title=""
|
||||
:width="expandMenu ? '100%' : '68%'"
|
||||
height="460px"
|
||||
theme="vs"
|
||||
:language="innerLanguagesType"
|
||||
:language="language"
|
||||
:read-only="props.disabled"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
|
@ -36,7 +36,7 @@
|
|||
<template #rightBox>
|
||||
<MsScriptMenu
|
||||
v-model:expand="expandMenu"
|
||||
v-model:languagesType="innerLanguagesType"
|
||||
v-model:languagesType="language"
|
||||
:disabled="props.disabled"
|
||||
@insert="insertHandler"
|
||||
@form-api-import="formApiImport"
|
||||
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
<MsCodeEditor
|
||||
v-else
|
||||
v-model:model-value="executionResultValue"
|
||||
v-model:model-value="executionResult"
|
||||
title=""
|
||||
width="100%"
|
||||
height="calc(100vh - 155px)"
|
||||
|
@ -60,7 +60,7 @@
|
|||
/>
|
||||
<InsertCommonScript
|
||||
v-model:visible="showInsertDrawer"
|
||||
:script-language="innerLanguagesType"
|
||||
:script-language="language"
|
||||
:enable-radio-selected="props.enableRadioSelected"
|
||||
@save="saveHandler"
|
||||
/>
|
||||
|
@ -73,7 +73,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import { Language, LanguageEnum } from '@/components/pure/ms-code-editor/types';
|
||||
|
@ -93,43 +92,27 @@
|
|||
defineProps<{
|
||||
showType: 'commonScript' | 'executionResult'; // 执行类型
|
||||
disabled?: boolean;
|
||||
language: Language;
|
||||
code: string;
|
||||
enableRadioSelected?: boolean;
|
||||
executionResult?: string; // 执行结果
|
||||
showHeader?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showHeader: true,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:language', value: Language): void;
|
||||
(e: 'update:code', value: string): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const projectId = ref<string>(appStore.currentProjectId);
|
||||
|
||||
const innerLanguagesType = useVModel(props, 'language', emit);
|
||||
const executionResultValue = useVModel(props, 'executionResult', emit);
|
||||
|
||||
watch(
|
||||
() => innerLanguagesType.value,
|
||||
(val) => {
|
||||
emit('update:language', val);
|
||||
}
|
||||
);
|
||||
|
||||
const innerCodeValue = useVModel(props, 'code', emit);
|
||||
|
||||
watch(
|
||||
() => props.code,
|
||||
(val) => {
|
||||
innerCodeValue.value = val;
|
||||
}
|
||||
);
|
||||
const language = defineModel<Language>('language', {
|
||||
required: true,
|
||||
});
|
||||
const executionResult = defineModel<string>('executionResult', {
|
||||
default: '',
|
||||
});
|
||||
const code = defineModel<string>('code', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const expandMenu = ref<boolean>(false);
|
||||
|
||||
|
@ -153,8 +136,8 @@
|
|||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
|
||||
function insertHandler(code: string) {
|
||||
codeEditorRef.value?.insertContent(code);
|
||||
function insertHandler(_code: string) {
|
||||
codeEditorRef.value?.insertContent(_code);
|
||||
}
|
||||
|
||||
function saveHandler(data: CommonScriptItem[]) {
|
||||
|
@ -185,7 +168,7 @@ ${item.script}
|
|||
}
|
||||
|
||||
function clearCode() {
|
||||
innerCodeValue.value = '';
|
||||
code.value = '';
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
@ -193,7 +176,7 @@ ${item.script}
|
|||
insertHandler,
|
||||
undoHandler,
|
||||
clearCode,
|
||||
executionResultValue,
|
||||
executionResult,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
v-permission="props.okPermission || []"
|
||||
type="secondary"
|
||||
:loading="props.okLoading"
|
||||
:disabled="okDisabled"
|
||||
@click="handleContinue"
|
||||
>
|
||||
{{ t(props.saveContinueText || 'ms.drawer.saveContinue') }}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
@press-enter="tagInputEnter"
|
||||
@blur="tagInputBlur"
|
||||
@clear="emit('clear')"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<template v-if="$slots.prefix" #prefix>
|
||||
<slot name="prefix"></slot>
|
||||
|
@ -66,7 +67,7 @@
|
|||
size: 'medium',
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(['update:modelValue', 'update:inputValue', 'change', 'clear', 'blur']);
|
||||
const emit = defineEmits(['update:modelValue', 'update:inputValue', 'change', 'clear', 'blur', 'click']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
|
@ -11,12 +11,17 @@ export default function useOpenNewPage() {
|
|||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
|
||||
function openNewPage(name: RouteRecordName | undefined, query = {}) {
|
||||
function openNewPage(name: RouteRecordName | undefined, query: Record<string, any> = {}) {
|
||||
const pId = query.pId || appStore.currentProjectId;
|
||||
if (pId) {
|
||||
// 如果传入参数指定了项目 id,则使用传入的项目 id
|
||||
delete query.pId;
|
||||
}
|
||||
const queryParams = new URLSearchParams(query).toString();
|
||||
window.open(
|
||||
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
|
||||
appStore.currentProjectId
|
||||
}&${queryParams}`,
|
||||
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${
|
||||
appStore.currentOrgId
|
||||
}&pId=${pId}&${queryParams}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ import {
|
|||
ExecuteApiRequestFullParams,
|
||||
ExecuteAssertionItem,
|
||||
ExecuteConditionConfig,
|
||||
ExecuteConditionProcessor,
|
||||
RequestResult,
|
||||
ResponseDefinition,
|
||||
} from './common';
|
||||
import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
|
||||
|
||||
// 场景-更新模块参数
|
||||
export interface ApiScenarioModuleUpdateParams {
|
||||
|
@ -190,7 +193,7 @@ export interface ScenarioHistoryItem {
|
|||
createUserName: string;
|
||||
versionName: string;
|
||||
}
|
||||
|
||||
// 场景步骤-自定义请求
|
||||
export type CustomApiStep = ExecuteApiRequestFullParams & {
|
||||
protocol: string;
|
||||
activeTab: RequestComposition;
|
||||
|
@ -241,6 +244,7 @@ export interface Variable {
|
|||
commonVariables: CommonVariable[];
|
||||
csvVariables: CsvVariable[];
|
||||
}
|
||||
// 场景配置
|
||||
export interface ScenarioConfig {
|
||||
variable: Variable;
|
||||
preProcessorConfig: ExecuteConditionConfig;
|
||||
|
@ -298,11 +302,13 @@ export interface LoopStepDetail extends StepDetailsCommon {
|
|||
msCountController: CountController;
|
||||
whileController: WhileController;
|
||||
}
|
||||
// 场景引用配置
|
||||
export interface ScenarioStepConfig {
|
||||
useCurrentScenarioParam: boolean; // 是否优先使用当前场景参数
|
||||
useBothScenarioParam: boolean; // 是否当前场景参数和源场景参数都应用(勾选非空值时为 true)
|
||||
enableScenarioEnv: boolean; // 是否应用源场景环境
|
||||
}
|
||||
// 场景步骤详情
|
||||
export type ScenarioStepDetail = Partial<
|
||||
CustomApiStepDetail &
|
||||
ConditionStepDetail &
|
||||
|
@ -312,14 +318,16 @@ export type ScenarioStepDetail = Partial<
|
|||
method: RequestMethods;
|
||||
}
|
||||
>;
|
||||
// 场景步骤项
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
sort: number;
|
||||
name: string;
|
||||
enable: boolean; // 是否启用
|
||||
copyFromStepId?: string; // 如果步骤是复制的,这个字段是复制的步骤id;如果复制的步骤也是复制的,并且没有加载过详情,则这个 id 是最原始的 被复制的步骤 id
|
||||
resourceId?: string; // 详情或者引用的类型才有
|
||||
resourceNum?: string; // 详情或者引用的类型才有
|
||||
resourceId?: string; // 详情或者引用、复制的类型才有
|
||||
resourceNum?: string; // 详情或者引用、复制的类型才有
|
||||
originProjectId?: string; // 如果步骤是复制的,这个字段是复制的资源的所在的项目id
|
||||
stepType: ScenarioStepType;
|
||||
refType: ScenarioStepRefType;
|
||||
config: ScenarioStepDetail; // 存储步骤列表需要展示的信息
|
||||
|
@ -342,6 +350,15 @@ export interface ScenarioStepItem {
|
|||
isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值
|
||||
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
|
||||
}
|
||||
// 场景步骤文件参数
|
||||
export interface ScenarioStepFileParams {
|
||||
uploadFileIds: string[];
|
||||
linkFileIds: string[];
|
||||
deleteFileIds?: string[];
|
||||
unLinkFileIds?: string[];
|
||||
}
|
||||
// 场景步骤详情
|
||||
export type ScenarioStepDetails = RequestParam | CaseRequestParam | ExecuteConditionProcessor;
|
||||
// 场景
|
||||
export interface Scenario {
|
||||
id?: string | number;
|
||||
|
@ -357,7 +374,8 @@ export interface Scenario {
|
|||
environmentId?: string;
|
||||
scenarioConfig: ScenarioConfig;
|
||||
steps: ScenarioStepItem[];
|
||||
stepDetails: Record<string, ScenarioStepDetail>;
|
||||
stepDetails: Record<string, ScenarioStepDetails>; // case、api、脚本操作抽屉的详情结构
|
||||
stepFileParam: Record<string, ScenarioStepFileParams>;
|
||||
follow?: boolean;
|
||||
uploadFileIds: string[];
|
||||
linkFileIds: string[];
|
||||
|
@ -375,6 +393,7 @@ export interface Scenario {
|
|||
isExecute?: boolean; // 是否从列表执行进去场景详情
|
||||
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤
|
||||
}
|
||||
// 场景详情
|
||||
export interface ScenarioDetail extends Scenario {
|
||||
stepTotal: number;
|
||||
requestPassRate: string;
|
||||
|
@ -390,7 +409,7 @@ export interface ScenarioDetail extends Scenario {
|
|||
updateTime: number;
|
||||
updateUser: string;
|
||||
}
|
||||
|
||||
// 场景-执行请求参数
|
||||
export interface ApiScenarioDebugRequest {
|
||||
id: string | number; // 场景 id
|
||||
grouped: boolean;
|
||||
|
@ -420,7 +439,7 @@ export interface ApiScenarioUpdateDTO extends Partial<Scenario> {
|
|||
deleteFileIds?: string[];
|
||||
unLinkFileIds?: string[];
|
||||
}
|
||||
|
||||
// 导入系统请求时获取步骤列表的分组入参
|
||||
export interface GetSystemRequestTypeParams {
|
||||
moduleIds?: (string | number)[];
|
||||
selectedIds: (string | number)[];
|
||||
|
@ -429,7 +448,7 @@ export interface GetSystemRequestTypeParams {
|
|||
protocol?: string;
|
||||
versionId?: string;
|
||||
}
|
||||
|
||||
// 导入系统请求时获取步骤列表的入参
|
||||
export interface GetSystemRequestParams {
|
||||
apiRequest?: GetSystemRequestTypeParams;
|
||||
caseRequest?: GetSystemRequestTypeParams;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useAppStore } from '@/store';
|
||||
import { clearToken, hasToken, isLoginExpires } from '@/utils/auth';
|
||||
|
||||
import NProgress from 'nprogress'; // progress bar
|
||||
|
|
|
@ -2,8 +2,6 @@ import { defineStore } from 'pinia';
|
|||
|
||||
import { getLicenseInfo } from '@/api/modules/setting/authorizedManagement';
|
||||
|
||||
import useAppStore from '../app';
|
||||
|
||||
const useLicenseStore = defineStore('license', {
|
||||
persist: true,
|
||||
state: (): { status: string | null } => ({
|
||||
|
@ -21,7 +19,6 @@ const useLicenseStore = defineStore('license', {
|
|||
},
|
||||
// license校验
|
||||
async getValidateLicense() {
|
||||
const appStore = useAppStore();
|
||||
try {
|
||||
const result = await getLicenseInfo();
|
||||
if (!result || !result.status || !result.license || !result.license.count) {
|
||||
|
|
|
@ -59,8 +59,8 @@ const useUserStore = defineStore('user', {
|
|||
} {
|
||||
const appStore = useAppStore();
|
||||
|
||||
state.userRoleRelations?.forEach(ug => {
|
||||
state.userRolePermissions?.forEach(gp => {
|
||||
state.userRoleRelations?.forEach((ug) => {
|
||||
state.userRolePermissions?.forEach((gp) => {
|
||||
if (gp.userRole.id === ug.roleId) {
|
||||
ug.userRolePermissions = gp.userRolePermissions;
|
||||
ug.userRole = gp.userRole;
|
||||
|
@ -151,13 +151,8 @@ const useUserStore = defineStore('user', {
|
|||
setToken(res.sessionId, res.csrfToken);
|
||||
this.setInfo(res);
|
||||
const { orgId, pId } = getHashParameters();
|
||||
// 如果访问页面的时候携带了组织 ID和项目 ID,则不设置
|
||||
if (!orgId || forceSet) {
|
||||
appStore.setCurrentOrgId(res.lastOrganizationId || '');
|
||||
}
|
||||
if (!pId || forceSet) {
|
||||
appStore.setCurrentProjectId(res.lastProjectId || '');
|
||||
}
|
||||
appStore.setCurrentOrgId(forceSet ? res.lastOrganizationId || '' : orgId || '');
|
||||
appStore.setCurrentProjectId(forceSet ? res.lastProjectId || '' : pId || '');
|
||||
return true;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
</div>
|
||||
<div class="flex items-center justify-between px-[12px] pt-[12px]">
|
||||
<div class="flex items-center">
|
||||
<a-tooltip :content="condition.name">
|
||||
<a-tooltip v-if="condition.name" :content="condition.name">
|
||||
<div class="script-name-container">
|
||||
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
|
||||
{{ condition.name }}
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
isPriorityLocalExec,
|
||||
execute,
|
||||
localExecuteUrl,
|
||||
hasLocalExec,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -279,6 +279,9 @@
|
|||
gap: 12px;
|
||||
}
|
||||
.code-container {
|
||||
@apply overflow-y-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
border-radius: var(--border-radius-small);
|
||||
|
|
|
@ -778,21 +778,24 @@
|
|||
emitChange('addTableLine addLineDisabled', isInit);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
rowIndex === paramsData.value.length - 1 &&
|
||||
(paramsData.value[rowIndex].key ||
|
||||
paramsData.value[rowIndex].projectId ||
|
||||
paramsData.value[rowIndex].header ||
|
||||
paramsData.value[rowIndex].variableName ||
|
||||
paramsData.value[rowIndex].expression)
|
||||
) {
|
||||
if (rowIndex === paramsData.value.length - 1) {
|
||||
// Don't change this!!!
|
||||
// 最后一行的更改才会触发添加新一行
|
||||
const id = new Date().getTime().toString();
|
||||
paramsData.value.push({
|
||||
const lastLineData = paramsData.value[rowIndex]; // 上一行数据
|
||||
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); // 找到下拉框选项的列
|
||||
const nextLine = {
|
||||
id,
|
||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||
enable: true, // 是否勾选
|
||||
} as any);
|
||||
} as any;
|
||||
selectColumnKeys.forEach((key) => {
|
||||
// 如果是更改了下拉框导致添加新的一列,需要将更改后的下拉框的值应用到下一行(产品为了方便统一输入参数类型)
|
||||
if (key) {
|
||||
nextLine[key] = lastLineData[key];
|
||||
}
|
||||
});
|
||||
paramsData.value.push(nextLine);
|
||||
}
|
||||
emitChange('addTableLine', isInit);
|
||||
handleMustContainColChange(true);
|
||||
|
|
|
@ -108,7 +108,6 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import caseTable from '../case/caseTable.vue';
|
||||
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
||||
import apiTable from './apiTable.vue';
|
||||
|
@ -230,15 +229,15 @@
|
|||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -169,15 +169,15 @@
|
|||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -63,15 +63,14 @@ export default {
|
|||
'apiTestManagement.importMode': 'Import mode',
|
||||
'apiTestManagement.importModeTip1': 'Cover:',
|
||||
'apiTestManagement.importModeTip2':
|
||||
'1.If the request type + path is the same for the same interface, if the request parameter content is inconsistent, it will be overwritten.',
|
||||
'1.The same interface already exists in the system (the request type + path are consistent). If the request parameter content is inconsistent, the original interface of the system will be overwritten.',
|
||||
'apiTestManagement.importModeTip3':
|
||||
'2.The same interface request type + path are the same, and the request parameter content is the same and does not change.',
|
||||
'apiTestManagement.importModeTip4':
|
||||
'3.If the interface is not the same and the request type + path is the same, add it',
|
||||
'2.The same interface already exists in the system (request type + path are consistent), if the content of the request parameters is consistent, no changes will be made.',
|
||||
'apiTestManagement.importModeTip4': '3.Interfaces that do not exist in the system are added.',
|
||||
'apiTestManagement.importModeTip5': 'Not covered:',
|
||||
'apiTestManagement.importModeTip6':
|
||||
'1.If the same interface request type + path are the same, no changes will be made.',
|
||||
'apiTestManagement.importModeTip7': '2.If the request type + path are not the same for the same interface, add a new',
|
||||
'1.If the same interface already exists in the system (the request type + path is the same), no changes will be made.',
|
||||
'apiTestManagement.importModeTip7': '2.Interfaces that do not exist in the system are added.',
|
||||
'apiTestManagement.cover': 'Cover',
|
||||
'apiTestManagement.uncover': 'Not covered',
|
||||
'apiTestManagement.moreSetting': 'More settings',
|
||||
|
|
|
@ -60,12 +60,12 @@ export default {
|
|||
'apiTestManagement.belongModule': '所属模块',
|
||||
'apiTestManagement.importMode': '导入模式',
|
||||
'apiTestManagement.importModeTip1': '覆盖:',
|
||||
'apiTestManagement.importModeTip2': '1.同一接口请求类型+路径一致,请求参数内容不一致则覆盖',
|
||||
'apiTestManagement.importModeTip3': '2.同一接口请求类型+路径一致,请求参数内容一致不做变更',
|
||||
'apiTestManagement.importModeTip4': '3.非同一接口,请求类型+路径一致,则新增',
|
||||
'apiTestManagement.importModeTip2': '1.系统已存在的同一接口(请求类型+路径一致),请求参数内容不一致则覆盖系统原接口',
|
||||
'apiTestManagement.importModeTip3': '2.系统已存在的同一接口(请求类型+路径一致),请求参数内容一致则不做变更',
|
||||
'apiTestManagement.importModeTip4': '3.系统不存在的接口,则新增',
|
||||
'apiTestManagement.importModeTip5': '不覆盖:',
|
||||
'apiTestManagement.importModeTip6': '1.同一接口请求类型+路径一致,则不做变更',
|
||||
'apiTestManagement.importModeTip7': '2.非同一接口请求类型+路径一致,则新增',
|
||||
'apiTestManagement.importModeTip6': '1.系统已存在的同一接口(请求类型+路径一致),则不做变更',
|
||||
'apiTestManagement.importModeTip7': '2.系统不存在的接口,则新增',
|
||||
'apiTestManagement.cover': '覆盖',
|
||||
'apiTestManagement.uncover': '不覆盖',
|
||||
'apiTestManagement.moreSetting': '更多设置',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
no-content-padding
|
||||
:show-continue="true"
|
||||
:footer="requestVModel.isNew === true"
|
||||
:ok-disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
@confirm="handleSave"
|
||||
@continue="handleContinue"
|
||||
@close="handleClose"
|
||||
|
@ -20,7 +21,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
|
||||
class="ml-auto flex items-center gap-[16px]"
|
||||
class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
|
||||
>
|
||||
<div class="text-[14px] font-normal text-[var(--color-text-4)]">
|
||||
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
|
||||
|
@ -28,6 +29,7 @@
|
|||
<a-select
|
||||
v-model:model-value="requestVModel.customizeRequestEnvEnable"
|
||||
class="w-[150px]"
|
||||
popup-container=".customApiDrawer-title-right"
|
||||
@change="handleUseEnvChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -318,7 +320,7 @@
|
|||
RequestResult,
|
||||
RequestTaskResult,
|
||||
} from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { ScenarioStepFileParams, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import {
|
||||
RequestAuthType,
|
||||
|
@ -366,6 +368,8 @@
|
|||
unSaved: boolean;
|
||||
uploadFileIds: string[];
|
||||
linkFileIds: string[];
|
||||
deleteFileIds?: string[];
|
||||
unLinkFileIds?: string[];
|
||||
}
|
||||
|
||||
export type RequestParam = ExecuteApiRequestFullParams & {
|
||||
|
@ -384,6 +388,7 @@
|
|||
update: string;
|
||||
};
|
||||
stepResponses?: Record<string | number, RequestResult>;
|
||||
fileParams?: ScenarioStepFileParams;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -399,6 +404,7 @@
|
|||
// 注入祖先组件提供的属性
|
||||
const scenarioId = inject<string | number>('scenarioId');
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
const hasLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
|
||||
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
|
@ -439,15 +445,15 @@
|
|||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
|
@ -656,8 +662,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const hasLocalExec = ref(false); // 是否配置了api本地执行
|
||||
|
||||
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); // 存储初始化过后的插件配置
|
||||
const temporaryPluginFormMap: Record<string, any> = {}; // 缓存插件表单,避免切换传入的 API 数据导致动态表单数据丢失
|
||||
const pluginLoading = ref(false);
|
||||
|
@ -909,8 +913,8 @@
|
|||
).validParams;
|
||||
parseRequestBodyResult = parseRequestBodyFiles(
|
||||
requestVModel.value.body,
|
||||
requestVModel.value.uploadFileIds, // 外面解析详情的时候传入
|
||||
requestVModel.value.linkFileIds // 外面解析详情的时候传入
|
||||
props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // 外面解析详情的时候传入,或引用 api 在requestVModel内存储
|
||||
props.fileParams?.linkFileIds || requestVModel.value.linkFileIds // 外面解析详情的时候传入,或引用 api 在requestVModel内存储
|
||||
);
|
||||
requestParams = {
|
||||
authConfig: requestVModel.value.authConfig,
|
||||
|
|
|
@ -266,7 +266,7 @@
|
|||
import { scrollIntoView } from '@/utils/dom';
|
||||
|
||||
import { ExecuteConditionConfig, PluginConfig, RequestResult } from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { ScenarioStepFileParams, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import {
|
||||
RequestAuthType,
|
||||
RequestBodyFormat,
|
||||
|
@ -298,6 +298,7 @@
|
|||
const props = defineProps<{
|
||||
request?: RequestParam; // 请求参数集合
|
||||
stepResponses?: Record<string | number, RequestResult>;
|
||||
fileParams?: ScenarioStepFileParams;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'applyStep', request: RequestParam): void;
|
||||
|
@ -351,15 +352,15 @@
|
|||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
|
@ -797,11 +798,14 @@
|
|||
defaultBodyParamsItem,
|
||||
isExecute
|
||||
).validParams;
|
||||
parseRequestBodyResult = parseRequestBodyFiles(
|
||||
requestVModel.value.body,
|
||||
requestVModel.value.uploadFileIds, // 外面解析详情的时候传入
|
||||
requestVModel.value.linkFileIds // 外面解析详情的时候传入
|
||||
);
|
||||
if (activeStep.value?.refType === ScenarioStepRefType.COPY) {
|
||||
// 复制 case 才能编辑,才需要计算
|
||||
parseRequestBodyResult = parseRequestBodyFiles(
|
||||
requestVModel.value.body,
|
||||
props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // 外面解析详情的时候传入,或引用 case 在requestVModel内存储
|
||||
props.fileParams?.linkFileIds || requestVModel.value.linkFileIds // 外面解析详情的时候传入,或引用 case 在requestVModel内存储
|
||||
);
|
||||
}
|
||||
requestParams = {
|
||||
authConfig: requestVModel.value.authConfig,
|
||||
body: {
|
||||
|
@ -878,8 +882,8 @@
|
|||
}
|
||||
|
||||
function handleClose() {
|
||||
// 关闭时若不是创建行为则是编辑行为,需要触发 applyStep
|
||||
if (!requestVModel.value.isNew) {
|
||||
// 关闭时若不是创建行为则是编辑行为,需要触发 applyStep,引用 case 不能更改不需要触发
|
||||
if (!requestVModel.value.isNew && activeStep.value?.refType === ScenarioStepRefType.COPY) {
|
||||
emit('applyStep', cloneDeep(makeRequestParams()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,7 +198,13 @@
|
|||
* 获取复制或引用的步骤数据
|
||||
* @param refType 复制或引用
|
||||
*/
|
||||
async function getScenarioSteps(refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF) {
|
||||
async function getScenarioSteps(
|
||||
refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF,
|
||||
other: {
|
||||
api: MsTableDataItem<ApiDefinitionDetail>[];
|
||||
case: MsTableDataItem<ApiCaseDetail>[];
|
||||
}
|
||||
) {
|
||||
const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {};
|
||||
// 可以跨项目选择,但是接口的项目 id 是单个,所以需要按项目分组
|
||||
selectedScenarios.value.forEach((e) => {
|
||||
|
@ -236,18 +242,19 @@
|
|||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
originProjectId: node.projectId,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}),
|
||||
name: `copy-${e.name}`,
|
||||
copyFromStepId: e.resourceId,
|
||||
originProjectId: e.projectId,
|
||||
};
|
||||
});
|
||||
emit(
|
||||
'copy',
|
||||
cloneDeep({
|
||||
api: selectedApis.value,
|
||||
case: selectedCases.value,
|
||||
api: other.api,
|
||||
case: other.case,
|
||||
scenario: fullScenarioArr,
|
||||
})
|
||||
);
|
||||
|
@ -260,18 +267,20 @@
|
|||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
originProjectId: node.projectId,
|
||||
id: getGenerateId(),
|
||||
isQuoteScenarioStep: true,
|
||||
isRefScenarioStep: true, // 默认是完全引用的
|
||||
};
|
||||
}),
|
||||
originProjectId: e.projectId,
|
||||
};
|
||||
});
|
||||
emit(
|
||||
'quote',
|
||||
cloneDeep({
|
||||
api: selectedApis.value,
|
||||
case: selectedCases.value,
|
||||
api: other.api,
|
||||
case: other.case,
|
||||
scenario: fullScenarioArr,
|
||||
})
|
||||
);
|
||||
|
@ -286,14 +295,25 @@
|
|||
}
|
||||
|
||||
async function handleCopy() {
|
||||
const copyApis = selectedApis.value.map((e) => ({
|
||||
...e,
|
||||
originProjectId: e.projectId,
|
||||
}));
|
||||
const copyCases = selectedCases.value.map((e) => ({
|
||||
...e,
|
||||
originProjectId: e.projectId,
|
||||
}));
|
||||
if (selectedScenarios.value.length > 0) {
|
||||
await getScenarioSteps(ScenarioStepRefType.COPY);
|
||||
await getScenarioSteps(ScenarioStepRefType.COPY, {
|
||||
api: copyApis,
|
||||
case: copyCases,
|
||||
});
|
||||
} else {
|
||||
emit(
|
||||
'copy',
|
||||
cloneDeep({
|
||||
api: selectedApis.value,
|
||||
case: selectedCases.value,
|
||||
api: copyApis,
|
||||
case: copyCases,
|
||||
scenario: selectedScenarios.value,
|
||||
})
|
||||
);
|
||||
|
@ -302,14 +322,25 @@
|
|||
}
|
||||
|
||||
async function handleQuote() {
|
||||
const quoteApis = selectedApis.value.map((e) => ({
|
||||
...e,
|
||||
originProjectId: e.projectId,
|
||||
}));
|
||||
const quoteCases = selectedCases.value.map((e) => ({
|
||||
...e,
|
||||
originProjectId: e.projectId,
|
||||
}));
|
||||
if (selectedScenarios.value.length > 0) {
|
||||
await getScenarioSteps(ScenarioStepRefType.REF);
|
||||
await getScenarioSteps(ScenarioStepRefType.REF, {
|
||||
api: quoteApis,
|
||||
case: quoteCases,
|
||||
});
|
||||
} else {
|
||||
emit(
|
||||
'quote',
|
||||
cloneDeep({
|
||||
api: selectedApis.value,
|
||||
case: selectedCases.value,
|
||||
api: quoteApis,
|
||||
case: quoteCases,
|
||||
scenario: selectedScenarios.value,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
:width="960"
|
||||
no-content-padding
|
||||
disabled-width-drag
|
||||
:footer="!props.detail"
|
||||
@close="handleClose"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<div class="ml-[16px] mt-[10px]">
|
||||
<!-- <stepTypeVue v-if="props.step" :step="props.step" /> -->
|
||||
{{ t('apiScenario.scriptOperationName') }}
|
||||
</div>
|
||||
<div class="ml-[16px] mt-[3px] max-w-[70%]">
|
||||
|
@ -19,9 +22,9 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="mt-[10px] flex h-[calc(100%-40px)] gap-[8px]">
|
||||
<conditionContent v-model:data="activeItem" :is-build-in="true" :is-format="true" />
|
||||
<conditionContent v-if="visible" v-model:data="activeItem" :is-build-in="true" :is-format="true" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<template v-if="!props.detail" #footer>
|
||||
<a-button type="secondary" @click="handleDrawerCancel">
|
||||
{{ t('common.cancel') }}
|
||||
</a-button>
|
||||
|
@ -36,19 +39,29 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import conditionContent from '@/views/api-test/components/condition/content.vue';
|
||||
|
||||
// import stepTypeVue from './stepType/stepType.vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { RequestConditionProcessor } from '@/enums/apiEnum';
|
||||
|
||||
const conditionContent = defineAsyncComponent(() => import('@/views/api-test/components/condition/content.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
script?: ExecuteConditionProcessor;
|
||||
detail?: ExecuteConditionProcessor;
|
||||
step?: ScenarioStepItem;
|
||||
name?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'add', name: string, scriptProcessor: ExecuteConditionProcessor): void;
|
||||
(e: 'save', name: string, scriptProcessor: ExecuteConditionProcessor): void;
|
||||
}>();
|
||||
|
||||
const defaultScript = {
|
||||
processorType: RequestConditionProcessor.SCRIPT,
|
||||
|
@ -56,44 +69,45 @@
|
|||
script: '',
|
||||
scriptLanguage: LanguageEnum.BEANSHELL_JSR233,
|
||||
commonScriptInfo: {},
|
||||
} as ExecuteConditionProcessor;
|
||||
const scriptName = ref(props.name || '');
|
||||
const activeItem = ref(props.script || defaultScript);
|
||||
polymorphicName: 'MsScriptElement',
|
||||
} as unknown as ExecuteConditionProcessor;
|
||||
const scriptName = ref('');
|
||||
const activeItem = ref<ExecuteConditionProcessor>(cloneDeep(defaultScript));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', name: string, scriptProcessor: ExecuteConditionProcessor): void;
|
||||
}>();
|
||||
|
||||
function resetField() {
|
||||
scriptName.value = '';
|
||||
activeItem.value = {
|
||||
processorType: RequestConditionProcessor.SCRIPT,
|
||||
enableCommonScript: false,
|
||||
script: '',
|
||||
scriptLanguage: LanguageEnum.BEANSHELL_JSR233,
|
||||
commonScriptInfo: {},
|
||||
} as ExecuteConditionProcessor;
|
||||
}
|
||||
watch(
|
||||
() => visible.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
scriptName.value = props.name || '';
|
||||
activeItem.value = cloneDeep(props.detail || defaultScript);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleDrawerCancel() {
|
||||
resetField();
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function saveAndContinue() {
|
||||
emit('save', scriptName.value, activeItem.value);
|
||||
resetField();
|
||||
emit('add', scriptName.value, activeItem.value);
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', scriptName.value, activeItem.value);
|
||||
resetField();
|
||||
emit('add', scriptName.value, activeItem.value);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
if (props.detail) {
|
||||
emit('save', scriptName.value, activeItem.value);
|
||||
}
|
||||
scriptName.value = '';
|
||||
activeItem.value = defaultScript as unknown as ExecuteConditionProcessor;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -94,11 +94,11 @@ export const defaultScenario: Scenario = {
|
|||
csvVariables: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
enableGlobal: true,
|
||||
processors: [],
|
||||
},
|
||||
assertionConfig: {
|
||||
|
@ -114,6 +114,7 @@ export const defaultScenario: Scenario = {
|
|||
},
|
||||
steps: [],
|
||||
stepDetails: {},
|
||||
stepFileParam: {},
|
||||
executeTime: 0,
|
||||
executeSuccessCount: 0,
|
||||
executeFailCount: 0,
|
||||
|
|
|
@ -116,9 +116,9 @@ export default function useCreateActions() {
|
|||
if (item.id || item.resourceId) {
|
||||
// 引用复制接口、用例、场景时的源资源信息
|
||||
resourceField = {
|
||||
resourceId: item.id || item.resourceId,
|
||||
resourceNum: item.num,
|
||||
resourceName: item.name,
|
||||
resourceId: item.id || item.resourceId, // 场景会调接口获取信息,所以有resourceId,接口、用例没有,下同
|
||||
resourceNum: item.num || item.resourceNum,
|
||||
resourceName: item.name || item.resourceName,
|
||||
};
|
||||
}
|
||||
if (item.protocol) {
|
||||
|
@ -141,6 +141,7 @@ export default function useCreateActions() {
|
|||
children: item.children || [],
|
||||
stepType,
|
||||
refType,
|
||||
originProjectId: item.originProjectId,
|
||||
copyFromStepId: item.copyFromStepId,
|
||||
...resourceField,
|
||||
name: name || item.name,
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
<template>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<!-- <a-popover position="bl" content-class="detail-popover" arrow-class="hidden">
|
||||
<a-popover
|
||||
v-if="
|
||||
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.API_SCENARIO].includes(props.data.stepType)
|
||||
"
|
||||
position="bl"
|
||||
content-class="detail-popover"
|
||||
arrow-class="hidden"
|
||||
>
|
||||
<MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<div>
|
||||
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
|
||||
<div class="text-[14px] text-[var(--color-text-1)]">
|
||||
{{ props.data.belongProjectName }}
|
||||
<!-- {{ props.data.belongProjectName }} -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -18,9 +25,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover> -->
|
||||
<!-- <MsTag
|
||||
v-if="props.data.projectId !== appStore.currentProjectId"
|
||||
</a-popover>
|
||||
<MsTag
|
||||
v-if="props.data.originProjectId !== appStore.currentProjectId"
|
||||
theme="outline"
|
||||
size="small"
|
||||
:self-style="{
|
||||
|
@ -30,22 +37,23 @@
|
|||
}"
|
||||
>
|
||||
{{ t('apiScenario.crossProject') }}
|
||||
</MsTag> -->
|
||||
</MsTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
// import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
// import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
// import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
|
||||
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
data: ScenarioStepItem;
|
||||
|
@ -53,27 +61,36 @@
|
|||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
// const { openNewPage } = useOpenNewPage();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
// function goDetail() {
|
||||
// const _stepType = getStepType(props.data);
|
||||
// switch (true) {
|
||||
// case _stepType.isCopyApi:
|
||||
// case _stepType.isQuoteApi:
|
||||
// openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id, pId: props.data.projectId });
|
||||
// break;
|
||||
// case _stepType.isCopyScenario:
|
||||
// case _stepType.isQuoteScenario:
|
||||
// openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id, pId: props.data.projectId });
|
||||
// break;
|
||||
// case _stepType.isQuoteCase:
|
||||
// case _stepType.isCopyCase:
|
||||
// openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id, pId: props.data.projectId });
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
function goDetail() {
|
||||
const _stepType = getStepType(props.data);
|
||||
switch (true) {
|
||||
case _stepType.isCopyApi:
|
||||
case _stepType.isQuoteApi:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
|
||||
pId: props.data.originProjectId,
|
||||
dId: props.data.resourceId,
|
||||
});
|
||||
break;
|
||||
case _stepType.isCopyScenario:
|
||||
case _stepType.isQuoteScenario:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
|
||||
pId: props.data.originProjectId,
|
||||
sId: props.data.resourceId,
|
||||
});
|
||||
break;
|
||||
case _stepType.isQuoteCase:
|
||||
case _stepType.isCopyCase:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
|
||||
pId: props.data.originProjectId,
|
||||
cId: props.data.resourceId,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<!-- 步骤差异内容,按步骤类型展示不同组件 -->
|
||||
<component
|
||||
:is="getStepContent(step)"
|
||||
:data="step.config"
|
||||
:data="checkStepIsApi(step) || step.stepType === ScenarioStepType.API_SCENARIO ? step : step.config"
|
||||
:step-id="step.id"
|
||||
@quick-input="setQuickInput(step, $event)"
|
||||
@change="handleStepContentChange($event, step)"
|
||||
|
@ -240,7 +240,8 @@
|
|||
</createStepActions>
|
||||
<customApiDrawer
|
||||
v-model:visible="customApiDrawerVisible"
|
||||
:request="currentStepDetail"
|
||||
:request="currentStepDetail as unknown as RequestParam"
|
||||
:file-params="currentStepFileParams"
|
||||
:step="activeStep"
|
||||
:step-responses="scenario.stepResponses"
|
||||
@add-step="addCustomApiStep"
|
||||
|
@ -251,7 +252,8 @@
|
|||
<customCaseDrawer
|
||||
v-model:visible="customCaseDrawerVisible"
|
||||
:active-step="activeStep"
|
||||
:request="currentStepDetail"
|
||||
:request="currentStepDetail as unknown as RequestParam"
|
||||
:file-params="currentStepFileParams"
|
||||
:step-responses="scenario.stepResponses"
|
||||
@apply-step="applyApiStep"
|
||||
@delete-step="deleteCaseStep(activeStep)"
|
||||
|
@ -265,11 +267,12 @@
|
|||
@quote="handleImportApiApply('quote', $event)"
|
||||
/>
|
||||
<scriptOperationDrawer
|
||||
v-if="scriptOperationDrawerVisible"
|
||||
v-model:visible="scriptOperationDrawerVisible"
|
||||
:script="currentStepDetail"
|
||||
:detail="currentStepDetail as unknown as ExecuteConditionProcessor"
|
||||
:step="activeStep"
|
||||
:name="activeStep?.name"
|
||||
@save="addScriptStep"
|
||||
@add="addScriptStep"
|
||||
@save="saveScriptStep"
|
||||
/>
|
||||
<a-modal
|
||||
v-model:visible="showQuickInput"
|
||||
|
@ -406,7 +409,6 @@
|
|||
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import responseResult from '@/views/api-test/components/requestComposition/response/index.vue';
|
||||
|
||||
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
||||
import { debugScenario, getScenarioStep } from '@/api/modules/api-test/scenario';
|
||||
|
@ -429,6 +431,8 @@
|
|||
CreateStepAction,
|
||||
Scenario,
|
||||
ScenarioStepConfig,
|
||||
ScenarioStepDetails,
|
||||
ScenarioStepFileParams,
|
||||
ScenarioStepItem,
|
||||
} from '@/models/apiTest/scenario';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
|
@ -442,6 +446,7 @@
|
|||
|
||||
import type { RequestParam } from '../common/customApiDrawer.vue';
|
||||
import useCreateActions from './createAction/useCreateActions';
|
||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
|
||||
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
|
||||
|
||||
|
@ -451,6 +456,9 @@
|
|||
const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue'));
|
||||
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
|
||||
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
|
||||
const responseResult = defineAsyncComponent(
|
||||
() => import('@/views/api-test/components/requestComposition/response/index.vue')
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
stepKeyword: string;
|
||||
|
@ -470,13 +478,14 @@
|
|||
required: true,
|
||||
});
|
||||
// 步骤详情映射,存储部分抽屉展示详情的数据
|
||||
const stepDetails = defineModel<Record<string, any>>('stepDetails', {
|
||||
const stepDetails = defineModel<Record<string, ScenarioStepDetails>>('stepDetails', {
|
||||
required: true,
|
||||
});
|
||||
const scenario = defineModel<Scenario>('scenario', {
|
||||
required: true,
|
||||
});
|
||||
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
|
||||
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,目前用来展示选中样式
|
||||
|
@ -510,11 +519,10 @@
|
|||
* 根据步骤类型获取步骤内容组件
|
||||
*/
|
||||
function getStepContent(step: ScenarioStepItem) {
|
||||
const _stepType = getStepType(step);
|
||||
if (_stepType.isQuoteApi || _stepType.isQuoteCase || _stepType.isQuoteScenario) {
|
||||
return quoteContent;
|
||||
}
|
||||
switch (step.stepType) {
|
||||
case ScenarioStepType.API:
|
||||
case ScenarioStepType.API_CASE:
|
||||
case ScenarioStepType.API_SCENARIO:
|
||||
case ScenarioStepType.CUSTOM_REQUEST:
|
||||
return quoteContent;
|
||||
case ScenarioStepType.LOOP_CONTROLLER:
|
||||
|
@ -741,11 +749,16 @@
|
|||
case 'copy':
|
||||
const id = getGenerateId();
|
||||
const stepDetail = stepDetails.value[node.id];
|
||||
const stepFileParam = scenario.value.stepFileParam[node.id];
|
||||
const { isQuoteScenario } = getStepType(node as ScenarioStepItem);
|
||||
if (stepDetail) {
|
||||
// 如果复制的步骤还有详情数据,则也复制详情数据
|
||||
stepDetails.value[id] = cloneDeep(stepDetail);
|
||||
}
|
||||
if (stepFileParam) {
|
||||
// 如果复制的步骤还有详情数据,则也复制详情数据
|
||||
scenario.value.stepFileParam[id] = cloneDeep(stepFileParam);
|
||||
}
|
||||
insertNodes<ScenarioStepItem>(
|
||||
steps.value,
|
||||
node.id,
|
||||
|
@ -754,11 +767,16 @@
|
|||
mapTree<ScenarioStepItem>(node, (childNode) => {
|
||||
const childId = getGenerateId();
|
||||
const childStepDetail = stepDetails.value[node.id];
|
||||
const childStepFileParam = scenario.value.stepFileParam[node.id];
|
||||
let childCopyFromStepId = childNode.id;
|
||||
if (childStepDetail) {
|
||||
// 如果复制的步骤下子步骤还有详情数据,则也复制详情数据
|
||||
stepDetails.value[childId] = cloneDeep(childStepDetail);
|
||||
}
|
||||
if (childStepFileParam) {
|
||||
// 如果复制的步骤还有详情数据,则也复制详情数据
|
||||
scenario.value.stepFileParam[id] = cloneDeep(childStepFileParam);
|
||||
}
|
||||
if (!isQuoteScenario) {
|
||||
// 非引用场景才处理复制来源 id
|
||||
if (childStepDetail || (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.REF)) {
|
||||
|
@ -892,24 +910,33 @@
|
|||
const customApiDrawerVisible = ref(false);
|
||||
const scriptOperationDrawerVisible = ref(false);
|
||||
const activeCreateAction = ref<CreateStepAction>(); // 用于抽屉操作创建步骤时记录当前插入类型
|
||||
const currentStepDetail = computed<any>(() => {
|
||||
// TODO: 步骤详情类型
|
||||
const currentStepDetail = computed<ScenarioStepDetails | undefined>(() => {
|
||||
if (activeStep.value) {
|
||||
return stepDetails.value[activeStep.value.id];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const currentStepFileParams = computed<ScenarioStepFileParams | undefined>(() => {
|
||||
if (activeStep.value) {
|
||||
return scenario.value.stepFileParam[activeStep.value.id];
|
||||
}
|
||||
});
|
||||
|
||||
async function getStepDetail(step: ScenarioStepItem) {
|
||||
try {
|
||||
appStore.showLoading();
|
||||
const res = await getScenarioStep(step.copyFromStepId || step.id);
|
||||
let parseRequestBodyResult;
|
||||
if (step.config.protocol === 'HTTP' && res.body) {
|
||||
parseRequestBodyResult = parseRequestBodyFiles(res.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
}
|
||||
stepDetails.value[step.id] = {
|
||||
...res,
|
||||
stepId: step.id,
|
||||
protocol: step.config.protocol,
|
||||
method: step.config.method,
|
||||
...parseRequestBodyResult,
|
||||
};
|
||||
scenario.value.stepFileParam[step.id] = parseRequestBodyResult;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -947,6 +974,13 @@
|
|||
customCaseDrawerVisible.value = true;
|
||||
} else if (step.stepType === ScenarioStepType.SCRIPT) {
|
||||
activeStep.value = step;
|
||||
if (
|
||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||
(stepDetails.value[step.id] === undefined && !step.isNew)
|
||||
) {
|
||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||
await getStepDetail(step);
|
||||
}
|
||||
scriptOperationDrawerVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
@ -971,16 +1005,11 @@
|
|||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(
|
||||
step: ScenarioStepItem,
|
||||
reportId: string | number,
|
||||
executeType?: 'localExec' | 'serverExec',
|
||||
localExecuteUrl?: string
|
||||
) {
|
||||
function debugSocket(step: ScenarioStepItem, reportId: string | number, executeType?: 'localExec' | 'serverExec') {
|
||||
websocketMap[reportId] = getSocket(
|
||||
reportId || '',
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl : ''
|
||||
executeType === 'localExec' ? localExecuteUrl?.value : ''
|
||||
);
|
||||
websocketMap[reportId].addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
@ -1022,13 +1051,12 @@
|
|||
ApiScenarioDebugRequest,
|
||||
'steps' | 'stepDetails' | 'reportId' | 'uploadFileIds' | 'linkFileIds'
|
||||
>,
|
||||
executeType?: 'localExec' | 'serverExec',
|
||||
localExecuteUrl?: string
|
||||
executeType?: 'localExec' | 'serverExec'
|
||||
) {
|
||||
const [currentStep] = executeParams.steps;
|
||||
try {
|
||||
currentStep.isExecuting = true;
|
||||
debugSocket(currentStep, executeParams.reportId, executeType, localExecuteUrl); // 开启websocket
|
||||
debugSocket(currentStep, executeParams.reportId, executeType); // 开启websocket
|
||||
const res = await debugScenario({
|
||||
id: scenario.value.id || '',
|
||||
grouped: false,
|
||||
|
@ -1044,8 +1072,8 @@
|
|||
};
|
||||
}),
|
||||
});
|
||||
if (executeType === 'localExec' && localExecuteUrl) {
|
||||
await localExecuteApiDebug(localExecuteUrl, res);
|
||||
if (executeType === 'localExec' && localExecuteUrl?.value) {
|
||||
await localExecuteApiDebug(localExecuteUrl.value, res);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -1073,15 +1101,16 @@
|
|||
}
|
||||
const stepDetail = stepDetails.value[realStep.id];
|
||||
delete scenario.value.stepResponses[realStep.id]; // 先移除上一次的执行结果
|
||||
const stepFileParam = scenario.value.stepFileParam[realStep.id];
|
||||
realExecute(
|
||||
{
|
||||
steps: [realStep as ScenarioStepItem],
|
||||
stepDetails: {
|
||||
[realStep.id]: stepDetails.value[realStep.id],
|
||||
[realStep.id]: stepDetail,
|
||||
},
|
||||
reportId: realStep.reportId,
|
||||
uploadFileIds: stepDetail?.uploadFileIds || [],
|
||||
linkFileIds: stepDetail?.linkFileIds || [],
|
||||
uploadFileIds: stepFileParam?.uploadFileIds || [],
|
||||
linkFileIds: stepFileParam?.linkFileIds || [],
|
||||
},
|
||||
isPriorityLocalExec?.value ? 'localExec' : 'serverExec'
|
||||
);
|
||||
|
@ -1245,6 +1274,12 @@
|
|||
customizeRequest: true,
|
||||
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
|
||||
};
|
||||
scenario.value.stepFileParam[request.stepId] = {
|
||||
linkFileIds: request.linkFileIds,
|
||||
uploadFileIds: request.uploadFileIds,
|
||||
deleteFileIds: request.deleteFileIds,
|
||||
unLinkFileIds: request.unLinkFileIds,
|
||||
};
|
||||
emit('updateResource', request.uploadFileIds, request.linkFileIds);
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
handleCreateStep(
|
||||
|
@ -1287,7 +1322,13 @@
|
|||
}
|
||||
if (activeStep.value) {
|
||||
request.isNew = false;
|
||||
stepDetails.value[activeStep.value?.id] = request;
|
||||
stepDetails.value[activeStep.value.id] = request;
|
||||
scenario.value.stepFileParam[activeStep.value?.id] = {
|
||||
linkFileIds: request.linkFileIds,
|
||||
uploadFileIds: request.uploadFileIds,
|
||||
deleteFileIds: request.deleteFileIds,
|
||||
unLinkFileIds: request.unLinkFileIds,
|
||||
};
|
||||
emit('updateResource', request.uploadFileIds, request.linkFileIds);
|
||||
activeStep.value = undefined;
|
||||
}
|
||||
|
@ -1337,6 +1378,14 @@
|
|||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
function saveScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
|
||||
if (activeStep.value) {
|
||||
stepDetails.value[activeStep.value.id] = cloneDeep(scriptProcessor);
|
||||
activeStep.value.name = name;
|
||||
activeStep.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放允许拖拽步骤到释放的节点内
|
||||
* @param dropNode 释放节点
|
||||
|
|
|
@ -529,6 +529,7 @@
|
|||
try {
|
||||
appStore.showLoading();
|
||||
const res = await getScenarioDetail(typeof record === 'string' ? record : record.id);
|
||||
res.stepFileParam = {};
|
||||
res.stepDetails = {};
|
||||
if (!res.steps) {
|
||||
res.steps = [];
|
||||
|
@ -551,11 +552,15 @@
|
|||
}
|
||||
});
|
||||
|
||||
const hasLocalExec = computed(() => executeButtonRef.value?.hasLocalExec);
|
||||
const localExecuteUrl = computed(() => executeButtonRef.value?.localExecuteUrl);
|
||||
const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec);
|
||||
const scenarioId = computed(() => activeScenarioTab.value.id);
|
||||
const scenarioExecuteLoading = computed(() => activeScenarioTab.value.executeLoading);
|
||||
// 为子孙组件提供属性
|
||||
provide('isPriorityLocalExec', readonly(isPriorityLocalExec));
|
||||
provide('hasLocalExec', readonly(hasLocalExec));
|
||||
provide('localExecuteUrl', readonly(localExecuteUrl));
|
||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||
provide('scenarioId', scenarioId);
|
||||
provide('scenarioExecuteLoading', scenarioExecuteLoading);
|
||||
|
|
|
@ -808,11 +808,11 @@
|
|||
:deep(.apiFieldIdClass) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.systemFieldWrapper) {
|
||||
.arco-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
|
@ -821,11 +821,11 @@
|
|||
background: var(--color-text-n9);
|
||||
}
|
||||
}
|
||||
|
||||
.tagWrapper {
|
||||
.arco-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
|
|
Loading…
Reference in New Issue