feat(接口场景): 场景步骤自定义请求保存&查看详情

This commit is contained in:
baiqi 2024-03-23 22:53:55 +08:00 committed by Craftsman
parent 960f1e3505
commit 886d566c21
39 changed files with 1111 additions and 747 deletions

View File

@ -1,6 +1,7 @@
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import { import {
AddModuleUrl, AddModuleUrl,
AddScenarioUrl,
BatchCopyScenarioUrl, BatchCopyScenarioUrl,
BatchDeleteScenarioUrl, BatchDeleteScenarioUrl,
BatchEditScenarioUrl, BatchEditScenarioUrl,
@ -13,6 +14,7 @@ import {
ExecuteHistoryUrl, ExecuteHistoryUrl,
GetModuleCountUrl, GetModuleCountUrl,
GetModuleTreeUrl, GetModuleTreeUrl,
GetScenarioUrl,
GetTrashModuleCountUrl, GetTrashModuleCountUrl,
GetTrashModuleTreeUrl, GetTrashModuleTreeUrl,
MoveModuleUrl, MoveModuleUrl,
@ -36,6 +38,8 @@ import {
ApiScenarioUpdateDTO, ApiScenarioUpdateDTO,
ExecuteHistoryItem, ExecuteHistoryItem,
ExecutePageParams, ExecutePageParams,
Scenario,
ScenarioDetail,
ScenarioHistoryItem, ScenarioHistoryItem,
ScenarioHistoryPageParams, ScenarioHistoryPageParams,
} from '@/models/apiTest/scenario'; } from '@/models/apiTest/scenario';
@ -180,3 +184,13 @@ export function batchDeleteScenario(data: {
}) { }) {
return MSR.post({ url: BatchDeleteScenarioUrl, data }); return MSR.post({ url: BatchDeleteScenarioUrl, data });
} }
// 添加场景
export function addScenario(params: Scenario) {
return MSR.post({ url: AddScenarioUrl, params });
}
// 获取场景详情
export function getScenarioDetail(id: string) {
return MSR.get<ScenarioDetail>({ url: GetScenarioUrl, params: id });
}

View File

@ -5,6 +5,8 @@ export const GetModuleCountUrl = '/api/scenario/module/count'; // 获取模块
export const AddModuleUrl = '/api/scenario/module/add'; // 添加模块 export const AddModuleUrl = '/api/scenario/module/add'; // 添加模块
export const DeleteModuleUrl = '/api/scenario/module/delete'; // 删除模块 export const DeleteModuleUrl = '/api/scenario/module/delete'; // 删除模块
export const ScenarioPageUrl = '/api/scenario/page'; // 接口场景列表 export const ScenarioPageUrl = '/api/scenario/page'; // 接口场景列表
export const AddScenarioUrl = '/api/scenario/add'; // 添加接口场景
export const GetScenarioUrl = '/api/scenario/get'; // 获取接口场景详情
export const UpdateScenarioUrl = '/api/scenario/update'; // 更新接口场景 export const UpdateScenarioUrl = '/api/scenario/update'; // 更新接口场景
export const RecycleScenarioUrl = '/api/scenario/delete-to-gc'; // 删除接口场景 export const RecycleScenarioUrl = '/api/scenario/delete-to-gc'; // 删除接口场景
export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景 export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景

View File

@ -146,8 +146,8 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExecuteAssertionConfig, ExecuteConditionProcessor } from '@/models/apiTest/common'; import { ExecuteAssertionConfig } from '@/models/apiTest/common';
import { RequestConditionScriptLanguage, ResponseAssertionType, ResponseBodyAssertionType } from '@/enums/apiEnum'; import { ResponseAssertionType, ResponseBodyAssertionType } from '@/enums/apiEnum';
import { ExecuteAssertion, MsAssertionItem } from './type'; import { ExecuteAssertion, MsAssertionItem } from './type';
@ -175,7 +175,6 @@
const innerConfig = useVModel(props, 'assertionConfig', emit); const innerConfig = useVModel(props, 'assertionConfig', emit);
const activeIds = ref('');
// Itemkey // Itemkey
const activeKey = ref<string>(''); const activeKey = ref<string>('');
// value // value

View File

@ -330,7 +330,7 @@
.handle { .handle {
@apply absolute left-0 top-0 flex h-full items-center; @apply absolute left-0 top-0 flex h-full items-center;
z-index: 1; z-index: 10;
width: 8px; width: 8px;
background-color: var(--color-neutral-3); background-color: var(--color-neutral-3);
cursor: col-resize; cursor: col-resize;

View File

@ -52,7 +52,7 @@
const checked = computed({ const checked = computed({
get: () => { get: () => {
return props.selectedKeys.size === props.total; return props.selectedKeys.size > 0 && props.selectedKeys.size === props.total;
}, },
set: (value) => { set: (value) => {
return value; return value;

View File

@ -231,6 +231,11 @@ export enum ScenarioCreateComposition {
// 接口场景详情组成部分 // 接口场景详情组成部分
export enum ScenarioDetailComposition { export enum ScenarioDetailComposition {
BASE_INFO = 'BASE_INFO', BASE_INFO = 'BASE_INFO',
STEP = 'STEP',
PARAMS = 'PARAMS',
PRE_POST = 'PRE_POST',
ASSERTION = 'ASSERTION',
SETTING = 'SETTING',
EXECUTE_HISTORY = 'EXECUTE_HISTORY', EXECUTE_HISTORY = 'EXECUTE_HISTORY',
CHANGE_HISTORY = 'CHANGE_HISTORY', CHANGE_HISTORY = 'CHANGE_HISTORY',
DEPENDENCY = 'DEPENDENCY', DEPENDENCY = 'DEPENDENCY',
@ -244,18 +249,6 @@ export enum ScenarioExecuteStatus {
} }
// 场景步骤类型 // 场景步骤类型
export enum ScenarioStepType { export enum ScenarioStepType {
QUOTE_API = 'QUOTE_API',
COPY_API = 'COPY_API',
QUOTE_CASE = 'QUOTE_CASE',
COPY_CASE = 'COPY_CASE',
QUOTE_SCENARIO = 'QUOTE_SCENARIO',
COPY_SCENARIO = 'COPY_SCENARIO',
WAIT_TIME = 'WAIT_TIME',
LOOP_CONTROL = 'LOOP_CONTROL',
CONDITION_CONTROL = 'CONDITION_CONTROL',
ONLY_ONCE_CONTROL = 'ONLY_ONCE_CONTROL',
SCRIPT_OPERATION = 'SCRIPT_OPERATION',
CUSTOM_API = 'CUSTOM_API',
API_CASE = 'API_CASE', // 接口用例 API_CASE = 'API_CASE', // 接口用例
LOOP_CONTROLLER = 'LOOP_CONTROLLER', // 循环控制器 LOOP_CONTROLLER = 'LOOP_CONTROLLER', // 循环控制器
API = 'API', // 接口定义 API = 'API', // 接口定义
@ -263,6 +256,14 @@ export enum ScenarioStepType {
API_SCENARIO = ' API_SCENARIO', // 场景 API_SCENARIO = ' API_SCENARIO', // 场景
IF_CONTROLLER = 'IF_CONTROLLER', // 条件控制器 IF_CONTROLLER = 'IF_CONTROLLER', // 条件控制器
ONCE_ONLY_CONTROLLER = 'ONCE_ONLY_CONTROLLER', // 一次控制器 ONCE_ONLY_CONTROLLER = 'ONCE_ONLY_CONTROLLER', // 一次控制器
CONSTANT_TIMER = 'CONSTANT_TIMER', // 等待控制器
SCRIPT = 'SCRIPT', // 脚本
}
export enum ScenarioStepRefType {
COPY = 'COPY', // 复制
DIRECT = 'DIRECT', // 在场景中直接创建的步骤 例如 自定义请求,逻辑控制器
PARTIAL_REF = 'PARTIAL_REF', // 部分引用
REF = 'REF', // 完全引用
} }
// 场景添加步骤操作类型 // 场景添加步骤操作类型
export enum ScenarioAddStepActionType { export enum ScenarioAddStepActionType {
@ -293,3 +294,25 @@ export enum ChangeHistoryStatusFilters {
IMPORT = 'IMPORT', IMPORT = 'IMPORT',
DELETE = 'DELETE', DELETE = 'DELETE',
} }
// 场景步骤-循环控制器类型
export enum ScenarioStepLoopTypeEnum {
WHILE = 'WHILE',
LOOP_COUNT = 'LOOP_COUNT',
FOREACH = 'FOREACH',
}
// 场景步骤-循环控制器-while循环类型
export enum WhileConditionType {
CONDITION = 'CONDITION',
SCRIPT = 'SCRIPT',
}
export enum ScenarioStepPolymorphicName {
COMMON_SCRIPT = 'MsCommentScriptElement',
IF_CONTROLLER = 'MsIfController',
LOOP_CONTROLLER = 'MsLoopController',
ONLY_ONCE = 'MsOnceOnlyController',
TIME_CONTROLLER = 'MsConstantTimerController',
}
export enum ScenarioFailureStrategy {
CONTINUE = 'CONTINUE',
STOP = 'STOP',
}

View File

@ -314,7 +314,7 @@ export type ExecuteAssertionItem = ResponseAssertionCommon &
ResponseVariableAssertion; ResponseVariableAssertion;
// 执行请求-断言配置 // 执行请求-断言配置
export interface ExecuteAssertionConfig { export interface ExecuteAssertionConfig {
enableGlobal: boolean; // 是否启用全局断言 enableGlobal?: boolean; // 是否启用全局断言,部分地方没有
assertions: ExecuteAssertionItem[]; assertions: ExecuteAssertionItem[];
} }
// 执行请求-共用配置子项 // 执行请求-共用配置子项

View File

@ -1,11 +1,28 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
import { ApiDefinitionCustomField, ApiRunModeRequest } from '@/models/apiTest/management'; import { ApiDefinitionCustomField, ApiRunModeRequest } from '@/models/apiTest/management';
import { ApiScenarioStatus, RequestComposition, RequestDefinitionStatus } from '@/enums/apiEnum'; import {
ApiScenarioStatus,
RequestAssertionCondition,
RequestComposition,
RequestDefinitionStatus,
RequestMethods,
ScenarioExecuteStatus,
ScenarioFailureStrategy,
ScenarioStepLoopTypeEnum,
ScenarioStepPolymorphicName,
ScenarioStepRefType,
ScenarioStepType,
WhileConditionType,
} from '@/enums/apiEnum';
import { BatchApiParams, TableQueryParams } from '../common'; import { BatchApiParams, TableQueryParams } from '../common';
import { ExecuteApiRequestFullParams, ResponseDefinition } from './common'; import {
ExecuteApiRequestFullParams,
ExecuteAssertionItem,
ExecuteConditionConfig,
ResponseDefinition,
} from './common';
// 场景-更新模块参数 // 场景-更新模块参数
export interface ApiScenarioModuleUpdateParams { export interface ApiScenarioModuleUpdateParams {
@ -27,11 +44,11 @@ export interface ApiScenarioGetModuleParams {
// 场景修改参数 // 场景修改参数
export interface ApiScenarioUpdateDTO { export interface ApiScenarioUpdateDTO {
id: string; id: string | number;
name?: string; name?: string;
priority?: string; priority?: string;
status?: ApiScenarioStatus; status?: ApiScenarioStatus;
moduleId?: string; moduleId?: string | number;
description?: string; description?: string;
tags?: string[]; tags?: string[];
grouped?: boolean; grouped?: boolean;
@ -196,25 +213,171 @@ export type CustomApiStep = ExecuteApiRequestFullParams & {
useEnv: string; useEnv: string;
}; };
// 场景步骤-循环控制器类型 // 场景步骤-循环控制器类型
export type ScenarioStepLoopType = 'num' | 'while' | 'forEach'; export type ScenarioStepLoopType = ScenarioStepLoopTypeEnum;
// 场景步骤-循环控制器-循环类型
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
// 场景步骤-步骤插入类型 // 场景步骤-步骤插入类型
export type CreateStepAction = 'inside' | 'before' | 'after'; export type CreateStepAction = 'inside' | 'before' | 'after';
// 场景步骤 export interface OtherConfig {
export interface Scenario { enableGlobalCookie: boolean;
enableCookieShare: boolean;
stepWaitTime: number;
enableStepWait: boolean;
failureStrategy: ScenarioFailureStrategy;
}
export interface AssertionConfig {
assertions: ExecuteAssertionItem[];
}
export interface CsvVariable {
id: string; id: string;
fileId: string;
scenarioId: string;
name: string;
fileName: string;
scope: string;
enable: boolean;
association: boolean;
encoding: string;
random: boolean;
variableNames: string;
ignoreFirstLine: boolean;
delimiter: string;
allowQuotedData: boolean;
recycleOnEof: boolean;
stopThreadOnEof: boolean;
}
export interface CommonVariable {
id: string | number;
key: string;
paramType: string;
value: string;
enable: boolean;
description: string;
tags: string[];
}
export interface Variable {
commonVariables: CommonVariable[];
csvVariables: CsvVariable[];
}
export interface ScenarioConfig {
variable: Variable;
preProcessorConfig: ExecuteConditionConfig;
postProcessorConfig: ExecuteConditionConfig;
assertionConfig: AssertionConfig;
otherConfig: OtherConfig;
}
export interface ForEachController {
loopTime: number; // 循环间隔时间
value: string; // 变量值
variable: string; // 变量名
}
export interface CountController {
loops: number; // 循环次数
}
export interface WhileScript {
scriptValue: string; // 脚本值
}
export interface WhileVariable {
condition: RequestAssertionCondition; // 条件操作符
value: string; // 变量值
variable: string; // 变量名
}
export interface WhileController {
conditionType: WhileConditionType; // 条件类型
timeout: number; // 超时时间
msWhileScript: WhileScript; // 脚本
msWhileVariable: WhileVariable; // 变量
}
export type ExtendedScenarioStepPolymorphicName = ScenarioStepPolymorphicName | string;
// 场景步骤详情公共部分
export interface StepDetailsCommon {
id: string | number;
copyFromStepId?: string; // 如果步骤是复制的这个字段是复制的步骤id
name: string;
enable: boolean;
polymorphicName: ExtendedScenarioStepPolymorphicName; // 多态名称,用于后台区分使用的是哪个组件
}
// 自定义请求
export interface CustomApiStepDetail extends StepDetailsCommon {
customizeRequest: boolean; // 是否自定义请求
customizeRequestEnvEnable: boolean; // 是否启用环境
}
// 条件控制器
export interface ConditionStepDetail extends StepDetailsCommon {
value: string; // 变量值
variable: string; // 变量名
condition: RequestAssertionCondition; // 条件操作符
}
// 循环控制器
export interface LoopStepDetail extends StepDetailsCommon {
loopType: ScenarioStepLoopType;
forEachController: ForEachController;
msCountController: CountController;
whileController: WhileController;
}
export type ScenarioStepDetail = Partial<CustomApiStepDetail & ConditionStepDetail & LoopStepDetail>;
export interface ScenarioStepItem {
id: string | number;
sort: number;
name: string;
executeStatus?: ScenarioExecuteStatus;
enable: boolean; // 是否启用
resourceId?: string; // 详情或者引用的类型才有
resourceNum?: string; // 详情或者引用的类型才有
stepType: ScenarioStepType;
refType: ScenarioStepRefType;
config?: ScenarioStepDetail; // 对应场景里stepDetails里的详情信息只有逻辑控制器需要
csvFileIds?: string[];
projectId?: string;
versionId?: string;
children?: ScenarioStepItem[];
// 页面渲染以及交互需要字段
checked?: boolean; // 是否选中
expanded?: boolean; // 是否展开
createActionsVisible?: boolean; // 是否展示创建步骤下拉
parent?: ScenarioStepItem; // 父级节点第一层的父级节点为undefined
resourceName?: string; // 引用复制接口、用例、场景时的源资源名称
method?: RequestMethods;
}
// 场景
export interface Scenario {
id?: string | number;
num?: number;
name: string; name: string;
moduleId: string | number; moduleId: string | number;
stepInfo: ScenarioStepInfo;
priority: CaseLevel; priority: CaseLevel;
status: RequestDefinitionStatus; status: ApiScenarioStatus;
tags: string[]; tags: string[];
params: Record<string, any>[]; projectId: string;
description: string;
grouped?: boolean;
environmentId?: string;
scenarioConfig: ScenarioConfig;
steps: ScenarioStepItem[];
stepDetails: Record<string, ScenarioStepDetail>;
follow?: boolean;
uploadFileIds: string[];
linkFileIds: string[];
// 前端渲染字段 // 前端渲染字段
label: string; label: string;
closable: boolean; closable: boolean;
isNew: boolean; isNew: boolean;
unSaved: boolean; unSaved: boolean;
executeLoading: boolean; // 执行loading executeLoading: boolean; // 执行loading
executeTime?: string | number; // 执行时间
executeSuccessCount?: number; // 执行成功数量
executeFailCount?: number; // 执行失败数量
}
export interface ScenarioDetail extends Scenario {
stepTotal: number;
requestPassRate: string;
lastReportStatus?: string;
lastReportId?: string;
deleted: boolean;
versionId: string;
refId: string;
latest: boolean;
modulePath: string;
createUser: string;
createTime: number;
updateTime: number;
updateUser: string;
} }

View File

@ -224,7 +224,7 @@ export function mapTree<T>(
return _tree return _tree
.map((node: TreeNode<T>, i: number) => { .map((node: TreeNode<T>, i: number) => {
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : ''; const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
node.order = i + 1; // order从 1 开始 node.sort = i + 1; // order从 1 开始
node.parent = _parent || undefined; // 没有父节点说明是树的第一层 node.parent = _parent || undefined; // 没有父节点说明是树的第一层
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node; const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
if (newNode) { if (newNode) {
@ -382,19 +382,19 @@ export function insertNodes<T>(
// 插入节点数组 // 插入节点数组
newNodes.forEach((newNode, index) => { newNodes.forEach((newNode, index) => {
newNode.parent = parent; newNode.parent = parent;
newNode.order = startOrder + index; newNode.sort = startOrder + index;
}); });
array.splice(startIndex, 0, ...newNodes); array.splice(startIndex, 0, ...newNodes);
} else { } else {
// 插入单个节点 // 插入单个节点
newNodes.parent = parent; newNodes.parent = parent;
newNodes.order = startOrder; newNodes.sort = startOrder;
array.splice(startIndex, 0, newNodes); array.splice(startIndex, 0, newNodes);
} }
// 更新插入节点之后的节点的 order // 更新插入节点之后的节点的 sort
const newLength = Array.isArray(newNodes) ? newNodes.length : 1; const newLength = Array.isArray(newNodes) ? newNodes.length : 1;
for (let j = startIndex + newLength; j < array.length; j++) { for (let j = startIndex + newLength; j < array.length; j++) {
array[j].order += newLength; array[j].sort += newLength;
} }
} }
@ -406,9 +406,9 @@ export function insertNodes<T>(
const parentChildren = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr const parentChildren = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
const index = parentChildren.findIndex((item) => item[customKey] === node[customKey]); const index = parentChildren.findIndex((item) => item[customKey] === node[customKey]);
if (position === 'before') { if (position === 'before') {
insertNewNodes(parentChildren, index, parent || node.parent, node.order); insertNewNodes(parentChildren, index, parent || node.parent, node.sort);
} else if (position === 'after') { } else if (position === 'after') {
insertNewNodes(parentChildren, index + 1, parent || node.parent, node.order + 1); insertNewNodes(parentChildren, index + 1, parent || node.parent, node.sort + 1);
} else if (position === 'inside') { } else if (position === 'inside') {
if (!node.children) { if (!node.children) {
node.children = []; node.children = [];
@ -460,9 +460,9 @@ export function handleTreeDragDrop<T>(
if (index !== -1) { if (index !== -1) {
parentChildren.splice(index, 1); parentChildren.splice(index, 1);
// 更新删除节点后的节点的 order // 更新删除节点后的节点的 sort
for (let i = index; i < parentChildren.length; i++) { for (let i = index; i < parentChildren.length; i++) {
parentChildren[i].order -= 1; parentChildren[i].sort -= 1;
} }
} }

View File

@ -857,6 +857,7 @@
nextTick(() => { nextTick(() => {
// form-create tab // form-create tab
fApi.value?.resetFields(); fApi.value?.resetFields();
isInitPluginForm.value = true;
}); });
} }
} }
@ -1136,7 +1137,9 @@
} }
reportId.value = getGenerateId(); reportId.value = getGenerateId();
requestVModel.value.reportId = reportId.value; // ID requestVModel.value.reportId = reportId.value; // ID
debugSocket(executeType); // websocket if (isExecute) {
debugSocket(executeType); // websocket
}
let requestName = ''; let requestName = '';
let requestModuleId = ''; let requestModuleId = '';
let apiDefinitionParams: Record<string, any> = {}; let apiDefinitionParams: Record<string, any> = {};

View File

@ -3,8 +3,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepType } from '@/enums/apiEnum';
@ -15,17 +13,19 @@
}>(); }>();
// //
const scenarioStepMap = { const scenarioStepMap = {
[ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' }, // [ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' }, // [ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' }, // [ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' }, // [ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' }, // [ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' }, // [ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' }, // [ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROLLER]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' }, [ScenarioStepType.LOOP_CONTROLLER]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' }, [ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' }, [ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' }, [ScenarioStepType.SCRIPT]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
// [ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
// [ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API_CASE]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' }, [ScenarioStepType.API_CASE]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.CUSTOM_REQUEST]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' }, [ScenarioStepType.CUSTOM_REQUEST]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API]: { label: 'report.detail.api', color: 'rgb(var(--link-4))' }, [ScenarioStepType.API]: { label: 'report.detail.api', color: 'rgb(var(--link-4))' },

View File

@ -5,10 +5,11 @@
<script setup lang="ts"> <script setup lang="ts">
import assertion from '@/components/business/ms-assertion/index.vue'; import assertion from '@/components/business/ms-assertion/index.vue';
import { AssertionConfig } from '@/models/apiTest/scenario';
const assertions = ref([]); const assertions = ref([]);
const assertionConfig = ref({ const assertionConfig = defineModel<AssertionConfig>('assertionConfig', {
enableGlobal: false, required: true,
assertions: [],
}); });
</script> </script>

View File

@ -0,0 +1,40 @@
<template>
<MsDescription :descriptions="descriptions"> </MsDescription>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioDetail } from '@/models/apiTest/scenario';
const props = defineProps<{
scenario: ScenarioDetail;
}>();
const { t } = useI18n();
const descriptions = computed<Description[]>(() => [
{
label: t('apiScenario.belongModule'),
value: props.scenario.modulePath,
},
{
label: t('apiScenario.table.columns.createUser'),
value: props.scenario.createUser,
},
{
label: t('apiScenario.table.columns.createTime'),
value: dayjs(props.scenario.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
label: t('apiScenario.table.columns.updateTime'),
value: dayjs(props.scenario.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
]);
</script>
<style lang="less" scoped></style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<a-alert v-if="isShowTip" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited"> <a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiScenario.historyListTip') }} {{ t('apiScenario.historyListTip') }}
<template #close-element> <template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span> <span class="text-[14px]">{{ t('common.notRemind') }}</span>
@ -11,7 +11,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -26,11 +25,10 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const isShowTip = ref<boolean>(true);
const visitedKey = 'scenarioHistoryTip'; const visitedKey = 'scenarioHistoryTip';
const { addVisited, getIsVisited } = useVisit(visitedKey); const { addVisited, getIsVisited } = useVisit(visitedKey);
const props = defineProps<{ const props = defineProps<{
sourceId: string | number; sourceId?: string | number;
}>(); }>();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {

View File

@ -11,10 +11,10 @@
> >
<template #title> <template #title>
<div class="flex items-center gap-[8px]"> <div class="flex items-center gap-[8px]">
<stepType <stepTypeVue
v-if="props.requestType" v-if="props.step"
v-show="props.requestType !== ScenarioStepType.CUSTOM_API" v-show="props.step.stepType !== ScenarioStepType.CUSTOM_REQUEST"
:type="props.requestType" :step="props.step"
/> />
{{ title }} {{ title }}
</div> </div>
@ -22,23 +22,24 @@
<div v-show="requestVModel.useEnv === 'false'" class="text-[14px] font-normal text-[var(--color-text-4)]"> <div v-show="requestVModel.useEnv === 'false'" class="text-[14px] font-normal text-[var(--color-text-4)]">
{{ t('apiScenario.env', { name: props.envDetailItem?.name }) }} {{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
</div> </div>
<MsSelect <a-select v-model:model-value="requestVModel.useEnv" class="w-[150px]" @change="handleUseEnvChange">
v-model:model-value="requestVModel.useEnv" <template #prefix>
:allow-search="false" <div> {{ t('project.environmental.env') }} </div>
:options="[ </template>
{ label: t('common.quote'), value: 'true' }, <a-option :value="true">{{ t('common.quote') }}</a-option>
{ label: t('common.notQuote'), value: 'false' }, <a-option :value="false">{{ t('common.notQuote') }}</a-option>
]" </a-select>
:multiple="false"
value-key="value"
label-key="label"
:prefix="t('project.environmental.env')"
class="w-[150px]"
@change="handleUseEnvChange"
>
</MsSelect>
</div> </div>
</template> </template>
<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>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col"> <div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div class="px-[18px] pt-[8px]"> <div class="px-[18px] pt-[8px]">
<div class="flex flex-wrap items-center justify-between gap-[12px]"> <div class="flex flex-wrap items-center justify-between gap-[12px]">
@ -48,7 +49,7 @@
v-model:model-value="requestVModel.protocol" v-model:model-value="requestVModel.protocol"
:options="protocolOptions" :options="protocolOptions"
:loading="protocolLoading" :loading="protocolLoading"
:disabled="props.requestType === ScenarioStepType.QUOTE_API" :disabled="_stepType.isQuoteApi"
class="w-[90px]" class="w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)" @change="(val) => handleActiveDebugProtocolChange(val as string)"
/> />
@ -60,15 +61,15 @@
is-tag is-tag
class="flex items-center" class="flex items-center"
/> />
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.label" :mouse-enter-delay="500"> <a-tooltip v-if="!isHttpProtocol" :content="requestVModel.name" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> {{ requestVModel.label }}</div> <div class="one-line-text max-w-[350px]"> {{ requestVModel.name }}</div>
</a-tooltip> </a-tooltip>
</div> </div>
<a-input-group v-if="isHttpProtocol" class="flex-1"> <a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect <apiMethodSelect
v-model:model-value="requestVModel.method" v-model:model-value="requestVModel.method"
class="w-[140px]" class="w-[140px]"
:disabled="props.requestType === ScenarioStepType.QUOTE_API" :disabled="_stepType.isQuoteApi"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<a-input <a-input
@ -78,7 +79,7 @@
allow-clear allow-clear
class="hover:z-10" class="hover:z-10"
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''" :style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
:disabled="props.requestType === ScenarioStepType.QUOTE_API" :disabled="_stepType.isQuoteApi"
@input="() => (isUrlError = false)" @input="() => (isUrlError = false)"
@change="handleUrlChange" @change="handleUrlChange"
/> />
@ -109,11 +110,7 @@
</div> </div>
</div> </div>
<a-input <a-input
v-if=" v-if="props.step?.stepType && (_stepType.isCopyApi || _stepType.isQuoteApi) && isHttpProtocol"
props.requestType &&
[ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(props.requestType) &&
isHttpProtocol
"
v-model:model-value="requestVModel.name" v-model:model-value="requestVModel.name"
:max-length="255" :max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')" :placeholder="t('apiTestManagement.apiNamePlaceholder')"
@ -130,7 +127,7 @@
class="no-content relative mt-[8px] border-b" class="no-content relative mt-[8px] border-b"
/> />
</div> </div>
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-87px)]"> <div ref="splitContainerRef" class="h-[calc(100%-87px)]">
<MsSplitBox <MsSplitBox
ref="verticalSplitBoxRef" ref="verticalSplitBoxRef"
v-model:size="splitBoxSize" v-model:size="splitBoxSize"
@ -150,7 +147,7 @@
> >
<div class="tab-pane-container"> <div class="tab-pane-container">
<a-spin <a-spin
v-if="requestVModel.activeTab === RequestComposition.PLUGIN" v-show="requestVModel.activeTab === RequestComposition.PLUGIN"
:loading="pluginLoading" :loading="pluginLoading"
class="min-h-[100px] w-full" class="min-h-[100px] w-full"
> >
@ -267,15 +264,6 @@
</MsSplitBox> </MsSplitBox>
</div> </div>
</div> </div>
<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" /> --> <!-- <addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" /> -->
</MsDrawer> </MsDrawer>
</template> </template>
@ -291,8 +279,7 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue'; import assertion from '@/components/business/ms-assertion/index.vue';
import MsSelect from '@/components/business/ms-select'; import stepTypeVue from './stepType/stepType.vue';
import stepType from './stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue'; import auth from '@/views/api-test/components/requestComposition/auth.vue';
@ -316,6 +303,7 @@
PluginConfig, PluginConfig,
RequestTaskResult, RequestTaskResult,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ModuleTreeNode, TransferFileParams } from '@/models/common'; import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { import {
RequestAuthType, RequestAuthType,
@ -327,6 +315,7 @@
ScenarioStepType, ScenarioStepType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import getStepType from './stepType/utils';
import { import {
defaultBodyParams, defaultBodyParams,
defaultBodyParamsItem, defaultBodyParamsItem,
@ -366,8 +355,7 @@
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
requestType?: ScenarioStepType; step?: ScenarioStepItem;
stepName: string;
detailLoading?: boolean; // detailLoading?: boolean; //
envDetailItem?: { envDetailItem?: {
id?: string; id?: string;
@ -402,12 +390,9 @@
type: 'api', type: 'api',
id: '', id: '',
useEnv: 'false', useEnv: 'false',
moduleId: 'root',
protocol: 'HTTP', protocol: 'HTTP',
url: '', url: '',
activeTab: RequestComposition.HEADER, activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSaved: false, unSaved: false,
headers: [], headers: [],
@ -415,9 +400,7 @@
query: [], query: [],
rest: [], rest: [],
polymorphicName: '', polymorphicName: '',
name: '',
path: '', path: '',
projectId: '',
uploadFileIds: [], uploadFileIds: [],
linkFileIds: [], linkFileIds: [],
authConfig: { authConfig: {
@ -455,16 +438,25 @@
followRedirects: true, followRedirects: true,
autoRedirects: false, autoRedirects: false,
}, },
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
responseActiveTab: ResponseComposition.BODY,
isNew: true, isNew: true,
executeLoading: false, executeLoading: false,
}; };
const requestVModel = ref<RequestParam>(props.request || defaultDebugParams); const requestVModel = ref<RequestParam>(defaultDebugParams);
const _stepType = computed(() => {
if (props.step) {
return getStepType(props.step);
}
return {
isCopyApi: false,
isQuoteApi: false,
};
});
const title = computed(() => { const title = computed(() => {
if (props.requestType && [ScenarioStepType.COPY_API, ScenarioStepType.QUOTE_API].includes(props.requestType)) { if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) {
return props.stepName; return props.step?.name;
} }
return t('apiScenario.customApi'); return t('apiScenario.customApi');
}); });
@ -604,11 +596,9 @@
const currentPluginScript = computed<Record<string, any>[]>( const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || [] () => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
); );
const isCopyApiNeedInit = computed( const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request?.request === null);
() => props.requestType === ScenarioStepType.COPY_API && props.request?.request === null
);
const isEditableApi = computed( const isEditableApi = computed(
() => props.requestType === ScenarioStepType.COPY_API || props.requestType === ScenarioStepType.CUSTOM_API () => _stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step
); );
// //
@ -649,7 +639,7 @@
controlPluginFormFields().forEach((key) => { controlPluginFormFields().forEach((key) => {
form[key] = formData[key]; form[key] = formData[key];
}); });
fApi.value?.setValue(form); fApi.value?.setValue(cloneDeep(form));
setTimeout(() => { setTimeout(() => {
// 300ms handlePluginFormChange // 300ms handlePluginFormChange
isInitPluginForm.value = true; isInitPluginForm.value = true;
@ -657,8 +647,10 @@
}); });
} }
} else { } else {
nextTick(() => { fApi.value?.nextTick(() => {
controlPluginFormFields(); controlPluginFormFields();
fApi.value?.resetFields();
isInitPluginForm.value = true;
}); });
} }
} }
@ -802,11 +794,13 @@
watch( watch(
() => showResponse.value, () => showResponse.value,
(val) => { (val) => {
if (val) { nextTick(() => {
changeVerticalExpand(true); if (val) {
} else { changeVerticalExpand(true);
changeVerticalExpand(false); } else {
} changeVerticalExpand(false);
}
});
} }
); );
@ -869,13 +863,13 @@
*/ */
function makeRequestParams(executeType?: 'localExec' | 'serverExec') { function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec'; const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find( const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol (e) => e.value === requestVModel.value.protocol
)?.polymorphicName; // )?.polymorphicName; //
let parseRequestBodyResult; let parseRequestBodyResult;
let requestParams; let requestParams;
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const realFormDataBodyValues = filterKeyValParams( const realFormDataBodyValues = filterKeyValParams(
formDataBody.formValues, formDataBody.formValues,
defaultBodyParamsItem, defaultBodyParamsItem,
@ -903,7 +897,6 @@
}, },
}, },
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams, headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
method: requestVModel.value.method,
otherConfig: requestVModel.value.otherConfig, otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path, path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams, query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
@ -917,40 +910,26 @@
polymorphicName, polymorphicName,
}; };
} }
reportId.value = getGenerateId(); if (isExecute) {
requestVModel.value.reportId = reportId.value; // ID debugSocket(executeType); // websocket
debugSocket(executeType); // websocket }
let requestName = '';
let requestModuleId = '';
const apiDefinitionParams: Record<string, any> = {};
requestName = requestVModel.value.name;
requestModuleId = requestVModel.value.moduleId;
return { return {
id: requestVModel.value.id.toString(), ...requestParams,
reportId: reportId.value, id: requestVModel.value.id,
environmentId: props.envDetailItem?.id || '', activeTab: requestVModel.value.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
name: requestName, responseActiveTab: ResponseComposition.BODY,
moduleId: requestModuleId,
...apiDefinitionParams,
protocol: requestVModel.value.protocol, protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol, method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
path: isHttpProtocol.value ? requestVModel.value.url || requestVModel.value.path : undefined, name: requestVModel.value.name,
request: { children: [
...requestParams, {
name: requestName, polymorphicName: 'MsCommonElement', // MsCommonElement
children: [ assertionConfig: requestVModel.value.children[0].assertionConfig,
{ postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
polymorphicName: 'MsCommonElement', // MsCommonElement preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
assertionConfig: requestVModel.value.children[0].assertionConfig, },
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig), ],
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
},
],
},
...parseRequestBodyResult, ...parseRequestBodyResult,
projectId: appStore.currentProjectId,
frontendDebug: executeType === 'localExec',
isNew: requestVModel.value.isNew,
}; };
} }
@ -959,7 +938,6 @@
* @param val 执行类型 * @param val 执行类型
*/ */
async function execute(executeType?: 'localExec' | 'serverExec') { async function execute(executeType?: 'localExec' | 'serverExec') {
// todo
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
try { try {
if (!props.executeApi) return; if (!props.executeApi) return;
@ -1008,8 +986,7 @@
} }
function handleContinue() { function handleContinue() {
requestVModel.value.isNew = false; // emit('addStep', cloneDeep(makeRequestParams()));
emit('addStep', requestVModel.value);
} }
function handleSave() { function handleSave() {
@ -1020,7 +997,7 @@
function handleClose() { function handleClose() {
// applyStep // applyStep
if (!requestVModel.value.isNew) { if (!requestVModel.value.isNew) {
emit('applyStep', { ...requestVModel.value, ...makeRequestParams() }); emit('applyStep', cloneDeep(makeRequestParams()));
} }
} }
@ -1037,7 +1014,6 @@
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
} }
requestVModel.value = { requestVModel.value = {
responseActiveTab: ResponseComposition.BODY,
executeLoading: false, executeLoading: false,
activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN, activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
unSaved: false, unSaved: false,
@ -1046,7 +1022,6 @@
...res.request, ...res.request,
...res, ...res,
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
url: res.path, url: res.path,
name: res.name, // requestnamenull name: res.name, // requestnamenull
id: res.id, id: res.id,
@ -1068,9 +1043,10 @@
async (val) => { async (val) => {
if (val) { if (val) {
if (props.request) { if (props.request) {
requestVModel.value = { ...defaultDebugParams, ...props.request }; console.log('props.request', props.request);
requestVModel.value = cloneDeep(props.request);
if ( if (
props.requestType === ScenarioStepType.QUOTE_API || _stepType.value.isQuoteApi ||
isCopyApiNeedInit.value isCopyApiNeedInit.value
// (request.requestrequest null) // (request.requestrequest null)
) { ) {
@ -1106,12 +1082,20 @@
// }); // });
// } // }
// } // }
} else {
requestVModel.value = cloneDeep({
...defaultDebugParams,
id: getGenerateId(),
});
} }
await initProtocolList(); await initProtocolList();
if (props.request) { if (props.request) {
handleActiveDebugProtocolChange(requestVModel.value.protocol); handleActiveDebugProtocolChange(requestVModel.value.protocol);
} }
} }
},
{
immediate: true,
} }
); );
</script> </script>

View File

@ -1,50 +0,0 @@
<template>
<div
class="text-nowrap rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{
borderColor: type.color,
color: type.color,
}"
>
{{ type.label }}
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{
type: ScenarioStepType;
}>();
const { t } = useI18n();
//
const scenarioStepMap = {
[ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROL]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.CONDITION_CONTROL]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONLY_ONCE_CONTROL]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
};
const type = computed(() => {
const config = scenarioStepMap[props.type];
return {
border: `1px solid ${config?.color}`,
color: config?.color,
label: t(config?.label),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,63 @@
<template>
<div
class="text-nowrap rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{
borderColor: type.color,
color: type.color,
}"
>
{{ type.label }}
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import getStepType from './utils';
const props = defineProps<{
step: ScenarioStepItem;
}>();
const { t } = useI18n();
//
const scenarioStepMap = {
[ScenarioStepType.CONSTANT_TIMER]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROLLER]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_REQUEST]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
};
const type = computed(() => {
let config = scenarioStepMap[props.step.stepType];
const stepType = getStepType(props.step);
if (!config) {
if (stepType.isQuoteApi) {
config = { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' };
} else if (stepType.isCopyApi) {
config = { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' };
} else if (stepType.isQuoteCase) {
config = { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' };
} else if (stepType.isCopyCase) {
config = { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' };
} else if (stepType.isQuoteScenario) {
config = { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' };
} else if (stepType.isCopyScenario) {
config = { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' };
}
}
return {
border: `1px solid ${config?.color}`,
color: config?.color,
label: t(config?.label),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,22 @@
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
export default function getStepType(step: ScenarioStepItem) {
const isCopyApi = step.stepType === ScenarioStepType.API && step.refType === ScenarioStepRefType.COPY;
const isQuoteApi = step.stepType === ScenarioStepType.API && step.refType === ScenarioStepRefType.REF;
const isCopyCase = step.stepType === ScenarioStepType.API_CASE && step.refType === ScenarioStepRefType.COPY;
const isQuoteCase = step.stepType === ScenarioStepType.API_CASE && step.refType === ScenarioStepRefType.REF;
const isCopyScenario = step.stepType === ScenarioStepType.API_SCENARIO && step.refType === ScenarioStepRefType.COPY;
const isQuoteScenario =
step.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(step.refType);
return {
isCopyApi,
isQuoteApi,
isCopyCase,
isQuoteCase,
isCopyScenario,
isQuoteScenario,
};
}

View File

@ -1,64 +1,144 @@
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario'; import { Scenario } from '@/models/apiTest/scenario';
import {
ApiScenarioStatus,
RequestAssertionCondition,
ScenarioFailureStrategy,
ScenarioStepLoopTypeEnum,
WhileConditionType,
} from '@/enums/apiEnum';
export const defaultStepItemCommon = { export const defaultStepItemCommon = {
checked: false, checked: false,
expanded: false, expanded: false,
enabled: true, enable: true,
children: [], children: [],
loopNum: 0, config: {
loopType: 'num' as ScenarioStepLoopType, id: '',
loopSpace: 0, copyFromStepId: '', // 如果步骤是复制的这个字段是复制的步骤id
variableName: '', name: '',
variablePrefix: '', enable: true,
loopWhileType: 'condition' as ScenarioStepLoopWhileType, polymorphicName: '', // 多态名称,用于后台区分使用的是哪个组件
variableVal: '', // 自定义请求
condition: 'equal', customizeRequest: false, // 是否自定义请求
overTime: 0, customizeRequestEnvEnable: false, // 是否启用环境
expression: '', // 条件控制器
waitTime: 0, value: '', // 变量值
description: '', variable: '', // 变量名
condition: RequestAssertionCondition.EQUALS, // 条件操作符
loopType: ScenarioStepLoopTypeEnum.LOOP_COUNT,
forEachController: {
loopTime: 0, // 循环间隔时间
value: '', // 变量值
variable: '', // 变量名
},
msCountController: {
loops: 0, // 循环次数
},
whileController: {
conditionType: WhileConditionType.CONDITION, // 条件类型
timeout: 0, // 超时时间
msWhileScript: {
scriptValue: '', // 脚本值
}, // 脚本
msWhileVariable: {
condition: RequestAssertionCondition.EQUALS, // 条件操作符
value: '', // 变量值
variable: '', // 变量名
}, // 变量
},
waitTime: 0, // 等待时间
},
createActionsVisible: false, createActionsVisible: false,
}; };
export const defaultScenario: Scenario = {
name: '',
moduleId: '',
priority: 'P0',
status: ApiScenarioStatus.UNDERWAY,
tags: [],
projectId: '',
description: '',
grouped: false,
environmentId: '',
scenarioConfig: {
variable: {
commonVariables: [],
csvVariables: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
assertionConfig: {
assertions: [],
},
otherConfig: {
enableGlobalCookie: true,
enableCookieShare: false,
enableStepWait: false,
stepWaitTime: 1000,
failureStrategy: ScenarioFailureStrategy.CONTINUE,
},
},
steps: [],
stepDetails: {},
executeTime: 0,
executeSuccessCount: 0,
executeFailCount: 0,
uploadFileIds: [],
linkFileIds: [],
// 前端渲染字段
label: '',
closable: true,
isNew: true,
unSaved: false,
executeLoading: false, // 执行loading
};
export const conditionOptions = [ export const conditionOptions = [
{ {
value: 'equal', value: RequestAssertionCondition.EQUALS,
label: 'apiScenario.equal', label: 'apiScenario.equal',
}, },
{ {
value: 'notEqualTo', value: RequestAssertionCondition.NOT_EQUALS,
label: 'apiScenario.notEqualTo', label: 'apiScenario.notEqualTo',
}, },
{ {
value: 'greater', value: RequestAssertionCondition.GT,
label: 'apiScenario.greater', label: 'apiScenario.greater',
}, },
{ {
value: 'less', value: RequestAssertionCondition.LT,
label: 'apiScenario.less', label: 'apiScenario.less',
}, },
{ {
value: 'greaterOrEqual', value: RequestAssertionCondition.GT_OR_EQUALS,
label: 'apiScenario.greaterOrEqual', label: 'apiScenario.greaterOrEqual',
}, },
{ {
value: 'lessOrEqual', value: RequestAssertionCondition.LT_OR_EQUALS,
label: 'apiScenario.lessOrEqual', label: 'apiScenario.lessOrEqual',
}, },
{ {
value: 'include', value: RequestAssertionCondition.CONTAINS,
label: 'apiScenario.include', label: 'apiScenario.include',
}, },
{ {
value: 'notInclude', value: RequestAssertionCondition.NOT_CONTAINS,
label: 'apiScenario.notInclude', label: 'apiScenario.notInclude',
}, },
{ {
value: 'null', value: RequestAssertionCondition.EMPTY,
label: 'apiScenario.null', label: 'apiScenario.null',
}, },
{ {
value: 'notNull', value: RequestAssertionCondition.NOT_EMPTY,
label: 'apiScenario.notNull', label: 'apiScenario.notNull',
}, },
]; ];

View File

@ -96,7 +96,6 @@
import { getExecuteHistory } from '@/api/modules/api-test/scenario'; import { getExecuteHistory } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ExecuteHistoryItem } from '@/models/apiTest/scenario'; import { ExecuteHistoryItem } from '@/models/apiTest/scenario';
import { ExecuteStatusFilters } from '@/enums/apiEnum'; import { ExecuteStatusFilters } from '@/enums/apiEnum';
@ -108,11 +107,10 @@
const statusFilters = ref(Object.keys(ExecuteStatusFilters)); const statusFilters = ref(Object.keys(ExecuteStatusFilters));
const tableQueryParams = ref<any>(); const tableQueryParams = ref<any>();
const appStore = useAppStore();
const keyword = ref(''); const keyword = ref('');
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
scenarioId: string; // id scenarioId?: string | number; // id
readOnly?: boolean; readOnly?: boolean;
}>(); }>();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
@ -174,7 +172,7 @@
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getExecuteHistory, getExecuteHistory,
{ {
columns, columns,

View File

@ -42,21 +42,22 @@
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue'; import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue'; import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { CommonVariable } from '@/models/apiTest/scenario';
import { defaultHeaderParamsItem } from '@/views/api-test/components/config'; import { defaultHeaderParamsItem } from '@/views/api-test/components/config';
import { filterKeyValParams } from '@/views/api-test/components/utils'; import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{ const props = defineProps<{
activeKey?: string; activeKey?: string;
params: any[]; params: CommonVariable[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', value: any[]): void; (e: 'update:params', value: CommonVariable[]): void;
(e: 'change'): void; // (e: 'change'): void; //
}>(); }>();
const innerParams = useVModel(props, 'params', emit); const innerParams = useVModel(props, 'params', emit);
const isShowTip = ref<boolean>(true); const isShowTip = ref<boolean>(true);
const { t } = useI18n(); const { t } = useI18n();
const activeRadio = ref('convention');
const searchValue = ref(''); const searchValue = ref('');
const firstSearch = ref(true); const firstSearch = ref(true);
const backupParams = ref(props.params); const backupParams = ref(props.params);

View File

@ -14,22 +14,22 @@
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue'; import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
import precondition from '@/views/api-test/components/requestComposition/precondition.vue'; import precondition from '@/views/api-test/components/requestComposition/precondition.vue';
import { ExecuteConditionConfig } from '@/models/apiTest/common';
const activeLayout = ref<'horizontal' | 'vertical'>('vertical'); const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const preProcessorConfig = ref({ const preProcessorConfig = defineModel<ExecuteConditionConfig>('preProcessorConfig', {
enableGlobal: false, required: true,
processors: [],
}); });
const postProcessorConfig = ref({ const postProcessorConfig = defineModel<ExecuteConditionConfig>('postProcessorConfig', {
enableGlobal: false, required: true,
processors: [],
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.condition { .condition {
flex-shrink: 0; overflow: overlay;
width: 100%; width: 100%;
height: 700px; height: 700px;
overflow: overlay; flex-shrink: 0;
} }
</style> </style>

View File

@ -171,7 +171,7 @@
readOnly: false, readOnly: false,
} }
); );
const emit = defineEmits(['init', 'newScenario', 'import', 'folderNodeSelect', 'clickScenario', 'changeProtocol']); const emit = defineEmits(['init', 'newScenario', 'import', 'folderNodeSelect', 'changeProtocol']);
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -129,7 +129,7 @@
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']" v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
type="text" type="text"
class="!mr-0" class="!mr-0"
@click="Message.info('// todo @ba1q1')" @click="openScenarioTab(record)"
> >
{{ t('apiScenario.execute') }} {{ t('apiScenario.execute') }}
</MsButton> </MsButton>
@ -138,7 +138,7 @@
v-permission="['PROJECT_API_SCENARIO:READ+ADD']" v-permission="['PROJECT_API_SCENARIO:READ+ADD']"
type="text" type="text"
class="!mr-0" class="!mr-0"
@click="Message.info('// todo @ba1q1')" @click="openScenarioTab(record, true)"
> >
{{ t('common.copy') }} {{ t('common.copy') }}
</MsButton> </MsButton>
@ -317,6 +317,7 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -349,6 +350,11 @@
offspringIds: string[]; offspringIds: string[];
readOnly?: boolean; // readOnly?: boolean; //
}>(); }>();
const emit = defineEmits<{
(e: 'openScenario', record: ApiScenarioTableItem, isCopy?: boolean): void;
(e: 'refreshModuleTree', params: any): void;
}>();
const lastReportStatusFilterVisible = ref(false); const lastReportStatusFilterVisible = ref(false);
const lastReportStatusListFilters = ref<string[]>(Object.keys(ReportStatus[ReportEnum.API_SCENARIO_REPORT])); const lastReportStatusListFilters = ref<string[]>(Object.keys(ReportStatus[ReportEnum.API_SCENARIO_REPORT]));
const lastReportStatusFilters = computed(() => { const lastReportStatusFilters = computed(() => {
@ -375,7 +381,6 @@
text: 'P3', text: 'P3',
}, },
]); ]);
const emit = defineEmits(['refreshModuleTree']);
const keyword = ref(''); const keyword = ref('');
const moveModalVisible = ref(false); const moveModalVisible = ref(false);
const isBatchMove = ref(false); // const isBatchMove = ref(false); //
@ -869,6 +874,7 @@
loadScenarioList(true); loadScenarioList(true);
resetSelector(); resetSelector();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
scenarioBatchOptTreeLoading.value = false; scenarioBatchOptTreeLoading.value = false;
@ -933,8 +939,8 @@
} }
} }
function openScenarioTab(record: ApiScenarioTableItem) { function openScenarioTab(record: ApiScenarioTableItem, isCopy = false) {
Message.info('// todo @ba1q1'); emit('openScenario', record, isCopy);
} }
defineExpose({ defineExpose({

View File

@ -12,11 +12,11 @@
</a-tooltip> </a-tooltip>
</div> </div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]"> <div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.envCookie" type="line" size="small" /> <a-switch v-model:model-value="form.enableGlobalCookie" type="line" size="small" />
{{ t('apiScenario.setting.environment.cookie') }} {{ t('apiScenario.setting.environment.cookie') }}
</div> </div>
<div class="mb-[16px] flex items-center gap-[8px]"> <div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.shareCookie" type="line" size="small" /> <a-switch v-model:model-value="form.enableCookieShare" type="line" size="small" />
{{ t('apiScenario.setting.share.cookie') }} {{ t('apiScenario.setting.share.cookie') }}
<a-tooltip :content="t('apiScenario.setting.share.cookie.tip')" position="right"> <a-tooltip :content="t('apiScenario.setting.share.cookie.tip')" position="right">
<div> <div>
@ -31,7 +31,7 @@
{{ t('apiScenario.setting.run.config') }} {{ t('apiScenario.setting.run.config') }}
</div> </div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]"> <div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.waitTime" type="line" size="small" /> <a-switch v-model:model-value="form.enableStepWait" type="line" size="small" />
{{ t('apiScenario.setting.step.waitTime') }} {{ t('apiScenario.setting.step.waitTime') }}
<a-tooltip :content="t('apiScenario.setting.waitTime.tip')"> <a-tooltip :content="t('apiScenario.setting.waitTime.tip')">
<div> <div>
@ -43,14 +43,14 @@
</a-tooltip> </a-tooltip>
</div> </div>
<a-form-item v-if="form.waitTime" class="flex-1"> <a-form-item v-if="form.stepWaitTime" class="flex-1">
<template #label> <template #label>
<div class="flex items-center"> <div class="flex items-center">
{{ t('apiScenario.setting.waitTime') }} {{ t('apiScenario.setting.waitTime') }}
<div class="text-[var(--color-text-brand)]">(ms)</div> <div class="text-[var(--color-text-brand)]">(ms)</div>
</div> </div>
</template> </template>
<a-input-number v-model:model-value="form.connectTimeout" mode="button" :step="100" :min="0" class="w-[160px]" /> <a-input-number v-model:model-value="form.stepWaitTime" mode="button" :step="100" :min="0" class="w-[160px]" />
</a-form-item> </a-form-item>
<a-form-item class="flex-1"> <a-form-item class="flex-1">
@ -59,9 +59,9 @@
{{ t('apiScenario.setting.step.rule') }} {{ t('apiScenario.setting.step.rule') }}
</div> </div>
</template> </template>
<a-radio-group v-model:model-value="form.rule"> <a-radio-group v-model:model-value="form.failureStrategy">
<a-radio value="ignore">{{ t('apiScenario.setting.step.rule.ignore') }}</a-radio> <a-radio :value="ScenarioFailureStrategy.CONTINUE">{{ t('apiScenario.setting.step.rule.ignore') }}</a-radio>
<a-radio value="stop">{{ t('apiScenario.setting.step.rule.stop') }}</a-radio> <a-radio :value="ScenarioFailureStrategy.STOP">{{ t('apiScenario.setting.step.rule.stop') }}</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -70,21 +70,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { OtherConfig } from '@/models/apiTest/scenario';
import { ScenarioFailureStrategy } from '@/enums/apiEnum';
const { t } = useI18n(); const { t } = useI18n();
// const emit = defineEmits(['update:formModeValue']); // ?
// const props = defineProps<{ const form = defineModel<OtherConfig>('otherConfig', {
// required: true,
// }>(); });
const initForm = {
envCookie: false,
shareCookie: false,
waitTime: false,
connectTimeout: 0,
rule: 'ignore',
};
const form = ref({ ...initForm });
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -45,13 +45,13 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import { ScenarioStepItem } from '../stepTree.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { findNodeByKey, getGenerateId } from '@/utils'; import { findNodeByKey, getGenerateId } from '@/utils';
import { CreateStepAction } from '@/models/apiTest/scenario'; import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import useCreateActions from './useCreateActions'; import useCreateActions from './useCreateActions';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config'; import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
@ -74,6 +74,7 @@
); );
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const visible = defineModel<boolean>('visible', { const visible = defineModel<boolean>('visible', {
@ -101,9 +102,10 @@
if (step.value && props.createStepAction) { if (step.value && props.createStepAction) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.LOOP_CONTROL, stepType: ScenarioStepType.LOOP_CONTROLLER,
name: t('apiScenario.loopControl'), name: t('apiScenario.loopControl'),
} as ScenarioStepItem, projectId: appStore.currentProjectId,
},
step.value, step.value,
steps.value, steps.value,
props.createStepAction, props.createStepAction,
@ -112,10 +114,12 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(), id: getGenerateId(),
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.LOOP_CONTROL, stepType: ScenarioStepType.LOOP_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.loopControl'), name: t('apiScenario.loopControl'),
projectId: appStore.currentProjectId,
}); });
} }
break; break;
@ -123,9 +127,10 @@
if (step.value && props.createStepAction) { if (step.value && props.createStepAction) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.CONDITION_CONTROL, stepType: ScenarioStepType.IF_CONTROLLER,
name: t('apiScenario.conditionControl'), name: t('apiScenario.conditionControl'),
} as ScenarioStepItem, projectId: appStore.currentProjectId,
},
step.value, step.value,
steps.value, steps.value,
props.createStepAction, props.createStepAction,
@ -134,10 +139,12 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(), id: getGenerateId(),
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.CONDITION_CONTROL, stepType: ScenarioStepType.IF_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.conditionControl'), name: t('apiScenario.conditionControl'),
projectId: appStore.currentProjectId,
}); });
} }
break; break;
@ -145,9 +152,10 @@
if (step.value && props.createStepAction) { if (step.value && props.createStepAction) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.ONLY_ONCE_CONTROL, stepType: ScenarioStepType.ONCE_ONLY_CONTROLLER,
name: t('apiScenario.onlyOnceControl'), name: t('apiScenario.onlyOnceControl'),
} as ScenarioStepItem, projectId: appStore.currentProjectId,
},
step.value, step.value,
steps.value, steps.value,
props.createStepAction, props.createStepAction,
@ -156,10 +164,12 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(), id: getGenerateId(),
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.ONLY_ONCE_CONTROL, stepType: ScenarioStepType.ONCE_ONLY_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.onlyOnceControl'), name: t('apiScenario.onlyOnceControl'),
projectId: appStore.currentProjectId,
}); });
} }
break; break;
@ -167,9 +177,10 @@
if (step.value && props.createStepAction) { if (step.value && props.createStepAction) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.WAIT_TIME, stepType: ScenarioStepType.CONSTANT_TIMER,
name: t('apiScenario.waitTime'), name: t('apiScenario.waitTime'),
} as ScenarioStepItem, projectId: appStore.currentProjectId,
},
step.value, step.value,
steps.value, steps.value,
props.createStepAction, props.createStepAction,
@ -178,10 +189,12 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(), id: getGenerateId(),
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.WAIT_TIME, stepType: ScenarioStepType.CONSTANT_TIMER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.waitTime'), name: t('apiScenario.waitTime'),
projectId: appStore.currentProjectId,
}); });
} }
break; break;
@ -189,7 +202,7 @@
case ScenarioAddStepActionType.CUSTOM_API: case ScenarioAddStepActionType.CUSTOM_API:
case ScenarioAddStepActionType.SCRIPT_OPERATION: case ScenarioAddStepActionType.SCRIPT_OPERATION:
if (step.value) { if (step.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.id, 'id');
if (realStep) { if (realStep) {
emit('otherCreate', val, realStep as ScenarioStepItem); emit('otherCreate', val, realStep as ScenarioStepItem);
} }

View File

@ -6,7 +6,7 @@
position="br" position="br"
@popup-visible-change="handleActionTriggerChange" @popup-visible-change="handleActionTriggerChange"
> >
<MsButton :id="step.stepId" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')"> <MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
<template #content> <template #content>
@ -63,13 +63,12 @@
<script setup lang="ts"> <script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ScenarioStepItem } from '../stepTree.vue';
import createStepActions from './createStepActions.vue'; import createStepActions from './createStepActions.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { CreateStepAction } from '@/models/apiTest/scenario'; import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
step: ScenarioStepItem; step: ScenarioStepItem;
@ -106,12 +105,15 @@
); );
const showAddChildStep = computed(() => { const showAddChildStep = computed(() => {
return [ return (
ScenarioStepType.LOOP_CONTROL, [
ScenarioStepType.CONDITION_CONTROL, ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.ONLY_ONCE_CONTROL, ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.COPY_SCENARIO, ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(innerStep.value.type); ].includes(innerStep.value.stepType) ||
(innerStep.value.stepType === ScenarioStepType.API_SCENARIO &&
innerStep.value.refType === ScenarioStepRefType.COPY)
);
}); });
const activeCreateAction = ref<CreateStepAction>(); const activeCreateAction = ref<CreateStepAction>();
@ -131,7 +133,7 @@
function handleActionsClose() { function handleActionsClose() {
activeCreateAction.value = undefined; activeCreateAction.value = undefined;
innerStep.value.createActionsVisible = false; innerStep.value.createActionsVisible = false;
document.getElementById(innerStep.value.stepId.toString())?.click(); document.getElementById(innerStep.value.id.toString())?.click();
} }
</script> </script>

View File

@ -1,12 +1,10 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { ScenarioStepItem } from '../stepTree.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { getGenerateId, insertNodes, TreeNode } from '@/utils'; import { getGenerateId, insertNodes, TreeNode } from '@/utils';
import { CreateStepAction } from '@/models/apiTest/scenario'; import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { defaultStepItemCommon } from '../../config'; import { defaultStepItemCommon } from '../../config';
@ -15,7 +13,7 @@ export default function useCreateActions() {
/** /**
* *
* @param selectedKeys stepId * @param selectedKeys id
* @param steps * @param steps
* @param parent * @param parent
*/ */
@ -24,9 +22,9 @@ export default function useCreateActions() {
steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[], steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem> parent?: TreeNode<ScenarioStepItem>
) { ) {
if (parent && selectedKeys.includes(parent.stepId)) { if (parent && selectedKeys.includes(parent.id)) {
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态) // 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
selectedKeys.push(...steps.map((item) => item.stepId)); selectedKeys.push(...steps.map((item) => item.id));
} }
} }
@ -36,10 +34,10 @@ export default function useCreateActions() {
* @param step * @param step
* @param steps * @param steps
* @param createStepAction * @param createStepAction
* @param selectedKeys stepId * @param selectedKeys id
*/ */
function handleCreateStep( function handleCreateStep(
defaultStepInfo: ScenarioStepItem, defaultStepInfo: Record<string, any>,
step: ScenarioStepItem, step: ScenarioStepItem,
steps: ScenarioStepItem[], steps: ScenarioStepItem[],
createStepAction: CreateStepAction, createStepAction: CreateStepAction,
@ -47,94 +45,71 @@ export default function useCreateActions() {
) { ) {
const newStep = { const newStep = {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(),
...defaultStepInfo, ...defaultStepInfo,
stepId: getGenerateId(),
}; };
switch (createStepAction) { console.log('newStep', newStep);
case 'inside':
newStep.order = step.children ? step.children.length : 0;
break;
case 'before':
newStep.order = step.order;
break;
case 'after':
default:
newStep.order = step.order + 1;
break;
}
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
step.parent?.children || steps, step.parent?.children || steps,
step.stepId, step.id,
newStep, newStep,
createStepAction, createStepAction,
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent), (newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
'stepId' 'id'
); );
} }
/** /**
* *
* @param newSteps * @param newSteps
* @param type * @param stepType
* @param startOrder * @param startOrder
*/ */
function buildInsertStepInfos( function buildInsertStepInfos(
newSteps: Record<string, any>[], newSteps: Record<string, any>[],
type: ScenarioStepType, stepType: ScenarioStepType,
refType: ScenarioStepRefType,
startOrder: number, startOrder: number,
stepsDetailMap: Record<string, any> stepDetails: Record<string, any>,
projectId: string
): ScenarioStepItem[] { ): ScenarioStepItem[] {
let name: string; let name: string;
switch (type) { switch (stepType) {
case ScenarioStepType.LOOP_CONTROL: case ScenarioStepType.LOOP_CONTROLLER:
name = t('apiScenario.loopControl'); name = t('apiScenario.loopControl');
break; break;
case ScenarioStepType.CONDITION_CONTROL: case ScenarioStepType.IF_CONTROLLER:
name = t('apiScenario.conditionControl'); name = t('apiScenario.conditionControl');
break; break;
case ScenarioStepType.ONLY_ONCE_CONTROL: case ScenarioStepType.ONCE_ONLY_CONTROLLER:
name = t('apiScenario.onlyOnceControl'); name = t('apiScenario.onlyOnceControl');
break; break;
case ScenarioStepType.WAIT_TIME: case ScenarioStepType.CONSTANT_TIMER:
name = t('apiScenario.waitTime'); name = t('apiScenario.waitTime');
break; break;
case ScenarioStepType.QUOTE_API: case ScenarioStepType.CUSTOM_REQUEST:
name = t('apiScenario.quoteApi');
break;
case ScenarioStepType.COPY_API:
name = t('apiScenario.copyApi');
break;
case ScenarioStepType.QUOTE_CASE:
name = t('apiScenario.quoteCase');
break;
case ScenarioStepType.COPY_CASE:
name = t('apiScenario.copyCase');
break;
case ScenarioStepType.QUOTE_SCENARIO:
name = t('apiScenario.quoteScenario');
break;
case ScenarioStepType.COPY_SCENARIO:
name = t('apiScenario.copyScenario');
break;
case ScenarioStepType.CUSTOM_API:
name = t('apiScenario.customApi'); name = t('apiScenario.customApi');
break; break;
case ScenarioStepType.SCRIPT_OPERATION: case ScenarioStepType.SCRIPT:
name = t('apiScenario.scriptOperation'); name = t('apiScenario.scriptOperation');
break; break;
default: default:
break; break;
} }
return newSteps.map((item, index) => { return newSteps.map((item, index) => {
const stepId = getGenerateId(); const id = getGenerateId();
stepsDetailMap[stepId] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息 stepDetails[id] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息
return { return {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
...item, ...item,
stepId, id,
type, stepType,
name, refType,
order: startOrder + index, resourceId: item.id,
resourceName: item.name,
name: name || item.name,
sort: startOrder + index,
projectId,
}; };
}); });
} }
@ -145,8 +120,7 @@ export default function useCreateActions() {
* @param readyInsertSteps buildInsertStepInfos得到构建后的步骤信息 * @param readyInsertSteps buildInsertStepInfos得到构建后的步骤信息
* @param steps * @param steps
* @param createStepAction * @param createStepAction
* @param type * @param selectedKeys id
* @param selectedKeys stepId
*/ */
function handleCreateSteps( function handleCreateSteps(
step: ScenarioStepItem, step: ScenarioStepItem,
@ -157,11 +131,11 @@ export default function useCreateActions() {
) { ) {
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
step.parent?.children || steps, step.parent?.children || steps,
step.stepId, step.id,
readyInsertSteps, readyInsertSteps,
createStepAction, createStepAction,
undefined, undefined,
'stepId' 'id'
); );
checkedIfNeed(selectedKeys, readyInsertSteps, step); checkedIfNeed(selectedKeys, readyInsertSteps, step);
} }

View File

@ -3,7 +3,7 @@
<div class="action-line"> <div class="action-line">
<div class="action-group"> <div class="action-group">
<a-checkbox <a-checkbox
v-show="stepInfo.steps.length > 0" v-show="scenario.steps.length > 0"
v-model:model-value="checkedAll" v-model:model-value="checkedAll"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@change="handleChangeAll" @change="handleChangeAll"
@ -17,7 +17,7 @@
<div class="action-group"> <div class="action-group">
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')"> <a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
<a-button <a-button
v-show="stepInfo.steps.length > 0" v-show="scenario.steps.length > 0"
type="outline" type="outline"
class="expand-step-btn arco-btn-outline--secondary" class="expand-step-btn arco-btn-outline--secondary"
size="mini" size="mini"
@ -42,20 +42,20 @@
</a-button> </a-button>
</template> </template>
</div> </div>
<template v-if="stepInfo.executeTime"> <template v-if="scenario.executeTime">
<div class="action-group"> <div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div>
<div class="text-[var(--color-text-4)]">{{ stepInfo.executeTime }}</div> <div class="text-[var(--color-text-4)]">{{ scenario.executeTime }}</div>
</div> </div>
<div class="action-group"> <div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.success') }}</div> <div class="text-[var(--color-text-1)]">{{ t('common.success') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeSuccessCount }}</div> <div class="text-[rgb(var(--success-6))]">{{ scenario.executeSuccessCount }}</div>
</div> </div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div> <div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeFailCount }}</div> <div class="text-[rgb(var(--success-6))]">{{ scenario.executeFailCount }}</div>
</div> </div>
<MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton> <MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton>
</div> </div>
@ -82,11 +82,11 @@
<div class="h-[calc(100%-48px)]"> <div class="h-[calc(100%-48px)]">
<stepTree <stepTree
ref="stepTreeRef" ref="stepTreeRef"
v-model:steps="stepInfo.steps" v-model:steps="scenario.steps"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:stepKeyword="keyword" v-model:stepKeyword="keyword"
:expand-all="isExpandAll" :expand-all="isExpandAll"
:steps-detail-map="stepInfo.stepsDetailMap" :step-details="scenario.stepDetails"
/> />
</div> </div>
</div> </div>
@ -116,29 +116,20 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import stepTree, { ScenarioStepItem } from './stepTree.vue'; import stepTree from './stepTree.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { countNodes } from '@/utils/tree'; import { countNodes } from '@/utils/tree';
export interface ScenarioStepInfo { import { Scenario } from '@/models/apiTest/scenario';
id: string | number;
steps: ScenarioStepItem[];
executeTime?: string; //
executeSuccessCount?: number; //
executeFailCount?: number; //
stepsDetailMap: Record<string, any>; //
}
const props = defineProps<{ const props = defineProps<{
isNew?: boolean; // isNew?: boolean; //
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const stepInfo = defineModel<ScenarioStepInfo>('step', { const scenario = defineModel<Scenario>('scenario', {
required: true, required: true,
}); });
@ -149,7 +140,7 @@
const stepTreeRef = ref<InstanceType<typeof stepTree>>(); const stepTreeRef = ref<InstanceType<typeof stepTree>>();
const keyword = ref(''); const keyword = ref('');
const totalStepCount = computed(() => countNodes(stepInfo.value.steps)); const totalStepCount = computed(() => countNodes(scenario.value.steps));
function handleChangeAll(value: boolean | (string | number | boolean)[]) { function handleChangeAll(value: boolean | (string | number | boolean)[]) {
indeterminate.value = false; indeterminate.value = false;
@ -202,7 +193,7 @@
try { try {
let ids = checkedKeys.value; let ids = checkedKeys.value;
if (batchToggleRange.value === 'top') { if (batchToggleRange.value === 'top') {
ids = stepInfo.value.steps.map((item) => item.stepId); ids = scenario.value.steps.map((item) => item.id);
} }
console.log('ids', ids); console.log('ids', ids);
await new Promise((resolve) => { await new Promise((resolve) => {

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="flex items-center gap-[4px]" draggable="false"> <div class="flex items-center gap-[4px]" draggable="false">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName"> <a-tooltip :content="innerData.variable" :disabled="!innerData.variable">
<a-input <a-input
v-model:model-value="innerData.variableName" v-model:model-value="innerData.variable"
size="mini" size="mini"
class="w-[100px] px-[8px]" class="w-[100px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })" :placeholder="t('apiScenario.variable', { suffix: '${var}' })"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -21,13 +21,13 @@
{{ t(opt.label) }} {{ t(opt.label) }}
</a-option> </a-option>
</a-select> </a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal"> <a-tooltip :content="innerData.value" :disabled="!innerData.value">
<a-input <a-input
:id="innerData.stepId" :id="innerData.id"
v-model:model-value="innerData.variableVal" v-model:model-value="innerData.value"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variableVal')" :placeholder="t('apiScenario.value')"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -38,21 +38,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ConditionStepDetail } from '@/models/apiTest/scenario';
import { conditionOptions } from '@/views/api-test/scenario/components/config'; import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface ConditionContentProps {
stepId: string;
variableName: string;
condition: string;
variableVal: string;
}
const props = defineProps<{ const props = defineProps<{
data: ConditionContentProps; data: ConditionStepDetail;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', innerData: ConditionContentProps): void; (e: 'change', innerData: ConditionStepDetail): void;
(e: 'quickInput', dataKey: keyof ConditionContentProps): void; (e: 'quickInput', dataKey: keyof ConditionStepDetail): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -75,8 +70,8 @@
() => dbClick?.value.timeStamp, () => dbClick?.value.timeStamp,
() => { () => {
// @ts-ignore // @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) { if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
emit('quickInput', 'variableVal'); emit('quickInput', 'value');
} }
} }
); );

View File

@ -9,12 +9,12 @@
@change="handleInputChange" @change="handleInputChange"
/> />
<a-tooltip <a-tooltip
v-if="innerData.loopType === 'num'" v-if="innerData.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT"
:content="innerData.loopNum.toString()" :content="innerData.msCountController.loops.toString()"
:disabled="!innerData.loopNum" :disabled="!innerData.msCountController.loops"
> >
<a-input-number <a-input-number
v-model:model-value="innerData.loopNum" v-model:model-value="innerData.msCountController.loops"
class="w-[80px] px-[8px]" class="w-[80px] px-[8px]"
size="mini" size="mini"
:step="1" :step="1"
@ -30,53 +30,56 @@
</a-input-number> </a-input-number>
</a-tooltip> </a-tooltip>
</a-input-group> </a-input-group>
<template v-if="innerData.loopType === 'forEach'"> <template v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName"> <a-tooltip :content="innerData.forEachController.variable" :disabled="!innerData.forEachController.variable">
<a-input <a-input
v-model:model-value="innerData.variableName" v-model:model-value="innerData.forEachController.variable"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variableName')" :placeholder="t('apiScenario.variable')"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
</a-tooltip> </a-tooltip>
<div class="font-medium">in</div> <div class="font-medium">in</div>
<a-tooltip :content="innerData.variablePrefix" :disabled="!innerData.variablePrefix"> <a-tooltip :content="innerData.forEachController.value" :disabled="!innerData.forEachController.value">
<a-input <a-input
v-model:model-value="innerData.variablePrefix" v-model:model-value="innerData.forEachController.value"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variablePrefix')" :placeholder="t('apiScenario.valuePrefix')"
:max-length="255" :max-length="255"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
</a-tooltip> </a-tooltip>
</template> </template>
<template v-else-if="innerData.loopType === 'while'"> <template v-else-if="innerData.loopType === ScenarioStepLoopTypeEnum.WHILE">
<a-select <a-select
v-model:model-value="innerData.loopWhileType" v-model:model-value="innerData.whileController.conditionType"
:options="whileOptions" :options="whileOptions"
size="mini" size="mini"
class="w-[75px] px-[8px]" class="w-[75px] px-[8px]"
@change="handleInputChange" @change="handleInputChange"
/> />
<template v-if="innerData.loopWhileType === 'condition'"> <template v-if="innerData.whileController.conditionType === WhileConditionType.CONDITION">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName"> <a-tooltip
:content="innerData.whileController.msWhileVariable.variable"
:disabled="!innerData.whileController.msWhileVariable.variable"
>
<a-input <a-input
v-model:model-value="innerData.variableName" v-model:model-value="innerData.whileController.msWhileVariable.variable"
size="mini" size="mini"
class="w-[100px] px-[8px]" class="w-[100px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })" :placeholder="t('apiScenario.variable', { suffix: '${var}' })"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
</a-tooltip> </a-tooltip>
<a-select <a-select
v-model:model-value="innerData.condition" v-model:model-value="innerData.whileController.msWhileVariable.condition"
size="mini" size="mini"
class="w-[90px] px-[8px]" class="w-[90px] px-[8px]"
@change="handleInputChange" @change="handleInputChange"
@ -85,22 +88,29 @@
{{ t(opt.label) }} {{ t(opt.label) }}
</a-option> </a-option>
</a-select> </a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal"> <a-tooltip
:content="innerData.whileController.msWhileVariable.value"
:disabled="!innerData.whileController.msWhileVariable.value"
>
<a-input <a-input
:id="innerData.stepId" :id="stepId"
v-model:model-value="innerData.variableVal" v-model:model-value="innerData.whileController.msWhileVariable.value"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variableVal')" :placeholder="t('apiScenario.value')"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
</a-tooltip> </a-tooltip>
</template> </template>
<a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression"> <a-tooltip
v-else
:content="innerData.whileController.msWhileScript.scriptValue"
:disabled="!innerData.whileController.msWhileScript.scriptValue"
>
<a-input <a-input
:id="innerData.stepId" :id="stepId"
v-model:model-value="innerData.expression" v-model:model-value="innerData.whileController.msWhileScript.scriptValue"
size="mini" size="mini"
class="w-[200px] px-[8px]" class="w-[200px] px-[8px]"
:placeholder="t('apiScenario.expression')" :placeholder="t('apiScenario.expression')"
@ -108,9 +118,9 @@
> >
</a-input> </a-input>
</a-tooltip> </a-tooltip>
<a-tooltip :content="innerData.overTime.toString()" :disabled="!innerData.overTime"> <a-tooltip :content="innerData.whileController.timeout.toString()" :disabled="!innerData.whileController.timeout">
<a-input-number <a-input-number
v-model:model-value="innerData.overTime" v-model:model-value="innerData.whileController.timeout"
class="w-[100px] px-[8px]" class="w-[100px] px-[8px]"
size="mini" size="mini"
:step="1" :step="1"
@ -121,18 +131,18 @@
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.overTime') }}:</div> <div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.timeout') }}:</div>
</template> </template>
</a-input-number> </a-input-number>
</a-tooltip> </a-tooltip>
</template> </template>
<a-tooltip <a-tooltip
v-if="innerData.loopType !== 'while'" v-if="innerData.loopType !== ScenarioStepLoopTypeEnum.WHILE"
:content="innerData.loopSpace.toString()" :content="innerData.forEachController.loopTime.toString()"
:disabled="!innerData.loopSpace" :disabled="!innerData.forEachController.loopTime"
> >
<a-input-number <a-input-number
v-model:model-value="innerData.loopSpace" v-model:model-value="innerData.forEachController.loopTime"
size="mini" size="mini"
:step="1" :step="1"
:min="0" :min="0"
@ -153,34 +163,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario'; import { LoopStepDetail } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepLoopTypeEnum, WhileConditionType } from '@/enums/apiEnum';
import { conditionOptions } from '@/views/api-test/scenario/components/config'; import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface LoopContentProps {
stepId: string | number;
num: number;
name: string;
type: ScenarioStepType;
loopNum: number;
loopType: ScenarioStepLoopType;
loopSpace: number;
variableName: string;
variablePrefix: string;
loopWhileType: ScenarioStepLoopWhileType;
variableVal: string;
condition: string;
overTime: number;
expression: string;
}
const props = defineProps<{ const props = defineProps<{
data: LoopContentProps; data: LoopStepDetail;
stepId: string | number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', innerData: LoopContentProps): void; (e: 'change', innerData: LoopStepDetail): void;
(e: 'quickInput', dataKey: keyof LoopContentProps): void; (e: 'quickInput', dataKey: string): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -188,25 +182,25 @@
const innerData = ref(props.data); const innerData = ref(props.data);
const loopOptions = [ const loopOptions = [
{ {
value: 'num', value: ScenarioStepLoopTypeEnum.LOOP_COUNT,
label: t('apiScenario.num'), label: t('apiScenario.num'),
}, },
{ {
value: 'while', value: ScenarioStepLoopTypeEnum.WHILE,
label: 'while', label: 'while',
}, },
{ {
value: 'forEach', value: ScenarioStepLoopTypeEnum.FOREACH,
label: 'forEach', label: 'forEach',
}, },
]; ];
const whileOptions = [ const whileOptions = [
{ {
value: 'condition', value: WhileConditionType.CONDITION,
label: t('apiScenario.condition'), label: t('apiScenario.condition'),
}, },
{ {
value: 'expression', value: WhileConditionType.SCRIPT,
label: t('apiScenario.expression'), label: t('apiScenario.expression'),
}, },
]; ];
@ -227,8 +221,13 @@
() => dbClick?.value.timeStamp, () => dbClick?.value.timeStamp,
() => { () => {
// @ts-ignore // @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) { if ((dbClick?.value.e?.target as Element).parentNode?.id === props.stepId) {
emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression'); emit(
'quickInput',
innerData.value.whileController.conditionType === WhileConditionType.CONDITION
? 'whileController.msWhileVariable.value'
: 'whileController.msWhileScript.scriptValue'
);
} }
} }
); );

View File

@ -7,20 +7,20 @@
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
<div class="text-[14px] text-[var(--color-text-1)]"> <div class="text-[14px] text-[var(--color-text-1)]">
{{ props.data.belongProjectName }} <!-- {{ props.data.belongProjectName }} -->
</div> </div>
</div> </div>
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div>
<div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail"> <div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail">
{{ `${props.data.num}${props.data.name}` }} {{ `${props.data.resourceNum}${props.data.resourceName}` }}
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</a-popover> </a-popover>
<MsTag <MsTag
v-if="props.data.belongProjectId !== props.data.currentProjectId" v-if="props.data.projectId !== appStore.currentProjectId"
theme="outline" theme="outline"
size="small" size="small"
:self-style="{ :self-style="{
@ -40,39 +40,35 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ApiTestRouteEnum } from '@/enums/routeEnum';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
const props = defineProps<{ const props = defineProps<{
data: { data: ScenarioStepItem;
id: string | number;
stepId: string | number;
belongProjectId: string;
belongProjectName: string;
num: number;
name: string;
type: ScenarioStepType;
currentProjectId: string;
};
}>(); }>();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openNewPage } = useOpenNewPage(); const { openNewPage } = useOpenNewPage();
function goDetail() { function goDetail() {
switch (props.data.type) { const _stepType = getStepType(props.data);
case ScenarioStepType.COPY_API: switch (true) {
case ScenarioStepType.QUOTE_API: case _stepType.isCopyApi:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id }); case _stepType.isQuoteApi:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id, pId: props.data.projectId });
break; break;
case ScenarioStepType.QUOTE_SCENARIO: case _stepType.isCopyScenario:
case ScenarioStepType.COPY_SCENARIO: case _stepType.isQuoteScenario:
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id }); openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id, pId: props.data.projectId });
break; break;
case ScenarioStepType.COPY_CASE: case _stepType.isQuoteCase:
case ScenarioStepType.QUOTE_CASE: case _stepType.isCopyCase:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id }); openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id, pId: props.data.projectId });
break; break;
default: default:
break; break;

View File

@ -24,7 +24,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
export interface WaitTimeContentProps { export interface WaitTimeContentProps {
stepId: string | number; id: string | number;
waitTime: number; waitTime: number;
} }

View File

@ -11,7 +11,7 @@
:expand-all="props.expandAll" :expand-all="props.expandAll"
:node-more-actions="stepMoreActions" :node-more-actions="stepMoreActions"
:filter-more-action-func="setStepMoreAction" :filter-more-action-func="setStepMoreAction"
:field-names="{ title: 'name', key: 'stepId', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
:virtual-list-props="{ :virtual-list-props="{
height: '100%', height: '100%',
threshold: 20, threshold: 20,
@ -37,7 +37,7 @@
<div <div
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white" class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
> >
{{ step.order }} {{ step.sort }}
</div> </div>
<div class="step-node-content"> <div class="step-node-content">
<!-- 步骤展开折叠按钮 --> <!-- 步骤展开折叠按钮 -->
@ -60,9 +60,9 @@
<div class="mr-[8px] flex items-center gap-[8px]"> <div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 --> <!-- 步骤启用/禁用 -->
<a-switch <a-switch
:default-checked="step.enabled" :default-checked="step.enable"
size="small" size="small"
@click.stop="step.enabled = !step.enabled" @click.stop="handleStepToggleEnable(step)"
></a-switch> ></a-switch>
<!-- 步骤执行 --> <!-- 步骤执行 -->
<MsIcon <MsIcon
@ -73,13 +73,14 @@
/> />
</div> </div>
<!-- 步骤类型 --> <!-- 步骤类型 -->
<stepType :type="step.type" /> <stepType :step="step" />
<!-- 步骤整体内容 --> <!-- 步骤整体内容 -->
<div class="relative flex flex-1 items-center gap-[4px]"> <div class="relative flex flex-1 items-center gap-[4px]">
<!-- 步骤差异内容按步骤类型展示不同组件 --> <!-- 步骤差异内容按步骤类型展示不同组件 -->
<component <component
:is="getStepContent(step)" :is="getStepContent(step)"
:data="step" :data="step.config"
:step-id="step.id"
@quick-input="setQuickInput(step, $event)" @quick-input="setQuickInput(step, $event)"
@change="handleStepContentChange($event, step)" @change="handleStepContentChange($event, step)"
@click.stop @click.stop
@ -88,7 +89,7 @@
<template v-if="checkStepIsApi(step)"> <template v-if="checkStepIsApi(step)">
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" /> <apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
<div <div
v-if="step.stepId === showStepNameEditInputStepId" v-if="step.id === showStepNameEditInputStepId"
class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]" class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
@click.stop @click.stop
> >
@ -117,12 +118,12 @@
<!-- 其他步骤描述 --> <!-- 其他步骤描述 -->
<template v-else> <template v-else>
<div <div
v-if="step.stepId === showStepDescEditInputStepId" v-if="step.id === showStepDescEditInputStepId"
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]" class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
> >
<a-input <a-input
v-model:model-value="tempStepDesc" v-model:model-value="tempStepDesc"
:default-value="step.description || t('apiScenario.pleaseInputStepDesc')" :default-value="step.name || t('apiScenario.pleaseInputStepDesc')"
:placeholder="t('apiScenario.pleaseInputStepDesc')" :placeholder="t('apiScenario.pleaseInputStepDesc')"
:max-length="255" :max-length="255"
size="small" size="small"
@ -135,14 +136,14 @@
</template> </template>
</a-input> </a-input>
</div> </div>
<a-tooltip :content="step.description" :disabled="!step.description"> <a-tooltip :content="step.name" :disabled="!step.name">
<div class="step-name-container"> <div class="step-name-container">
<div <div
:class="`one-line-text mr-[4px] ${ :class="`one-line-text mr-[4px] ${
step.type === ScenarioStepType.ONLY_ONCE_CONTROL ? 'max-w-[750px]' : 'max-w-[150px]' step.stepType === ScenarioStepType.ONCE_ONLY_CONTROLLER ? 'max-w-[750px]' : 'max-w-[150px]'
} font-normal text-[var(--color-text-4)]`" } font-normal text-[var(--color-text-4)]`"
> >
{{ step.description || t('apiScenario.pleaseInputStepDesc') }} {{ step.name || t('apiScenario.pleaseInputStepDesc') }}
</div> </div>
<MsIcon <MsIcon
type="icon-icon_edit_outlined" type="icon-icon_edit_outlined"
@ -161,7 +162,7 @@
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:steps="steps" v-model:steps="steps"
:step="step" :step="step"
@click="setFocusNodeKey(step.stepId)" @click="setFocusNodeKey(step.id)"
@other-create="handleOtherCreate" @other-create="handleOtherCreate"
@close="setFocusNodeKey('')" @close="setFocusNodeKey('')"
/> />
@ -190,8 +191,7 @@
v-model:visible="customApiDrawerVisible" v-model:visible="customApiDrawerVisible"
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }" :env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
:request="currentStepDetail" :request="currentStepDetail"
:request-type="activeStep?.type" :step="activeStep"
:step-name="activeStep?.name || ''"
@add-step="addCustomApiStep" @add-step="addCustomApiStep"
@apply-step="applyApiStep" @apply-step="applyApiStep"
/> />
@ -257,7 +257,7 @@
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue'; import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue'; import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue'; import stepType from '../common/stepType/stepType.vue';
import createStepActions from './createAction/createStepActions.vue'; import createStepActions from './createAction/createStepActions.vue';
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue'; import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
import conditionContent from './stepNodeComposition/conditionContent.vue'; import conditionContent from './stepNodeComposition/conditionContent.vue';
@ -281,11 +281,12 @@
} from '@/utils'; } from '@/utils';
import { ExecuteConditionProcessor } from '@/models/apiTest/common'; import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario'; import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import type { RequestParam } from '../common/customApiDrawer.vue'; import type { RequestParam } from '../common/customApiDrawer.vue';
import useCreateActions from './createAction/useCreateActions'; import useCreateActions from './createAction/useCreateActions';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config'; import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
// //
@ -295,41 +296,6 @@
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue')); const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue')); const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
export interface ScenarioStepItem {
stepId: string | number;
order: number;
enabled: boolean; //
type: ScenarioStepType;
name: string;
description: string;
method?: RequestMethods;
executeStatus?: ScenarioExecuteStatus;
num?: number; //
//
belongProjectId?: string;
belongProjectName?: string;
children?: ScenarioStepItem[];
//
request?: RequestParam;
//
script?: ExecuteConditionProcessor;
//
// renderId: string; // id
checked: boolean; //
expanded: boolean; //
createActionsVisible?: boolean; //
parent?: ScenarioStepItem; // undefined
loopNum: number;
loopType: 'num' | 'while' | 'forEach';
loopSpace: number;
variableName: string;
variablePrefix: string;
loopWhileType: ScenarioStepLoopWhileType;
variableVal: string;
condition: string;
overTime: number;
}
const props = defineProps<{ const props = defineProps<{
stepKeyword: string; stepKeyword: string;
expandAll?: boolean; expandAll?: boolean;
@ -345,7 +311,7 @@
required: true, required: true,
}); });
// //
const stepsDetailMap = defineModel<Record<string, any>>('stepsDetailMap', { const stepDetails = defineModel<Record<string, any>>('stepDetails', {
required: true, required: true,
}); });
@ -358,18 +324,18 @@
* 根据步骤类型获取步骤内容组件 * 根据步骤类型获取步骤内容组件
*/ */
function getStepContent(step: ScenarioStepItem) { function getStepContent(step: ScenarioStepItem) {
switch (step.type) { const _stepType = getStepType(step);
case ScenarioStepType.QUOTE_API: if (_stepType.isQuoteApi || _stepType.isQuoteCase || _stepType.isQuoteScenario) {
case ScenarioStepType.QUOTE_CASE: return quoteContent;
case ScenarioStepType.QUOTE_SCENARIO: }
return quoteContent; switch (step.stepType) {
case ScenarioStepType.CUSTOM_API: case ScenarioStepType.CUSTOM_REQUEST:
return customApiContent; return customApiContent;
case ScenarioStepType.LOOP_CONTROL: case ScenarioStepType.LOOP_CONTROLLER:
return loopControlContent; return loopControlContent;
case ScenarioStepType.CONDITION_CONTROL: case ScenarioStepType.IF_CONTROLLER:
return conditionContent; return conditionContent;
case ScenarioStepType.WAIT_TIME: case ScenarioStepType.CONSTANT_TIMER:
return waitTimeContent; return waitTimeContent;
default: default:
return () => null; return () => null;
@ -381,34 +347,25 @@
} }
function checkStepIsApi(step: ScenarioStepItem) { function checkStepIsApi(step: ScenarioStepItem) {
return [ return [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType);
ScenarioStepType.QUOTE_API,
ScenarioStepType.COPY_API,
ScenarioStepType.QUOTE_CASE,
ScenarioStepType.COPY_CASE,
ScenarioStepType.CUSTOM_API,
].includes(step.type);
} }
function checkStepShowMethod(step: ScenarioStepItem) { function checkStepShowMethod(step: ScenarioStepItem) {
return [ return [
ScenarioStepType.QUOTE_API, ScenarioStepType.API,
ScenarioStepType.COPY_API, ScenarioStepType.API_CASE,
ScenarioStepType.QUOTE_CASE, ScenarioStepType.CUSTOM_REQUEST,
ScenarioStepType.COPY_CASE, ScenarioStepType.API_SCENARIO,
ScenarioStepType.CUSTOM_API, ].includes(step.stepType);
ScenarioStepType.QUOTE_SCENARIO,
ScenarioStepType.COPY_SCENARIO,
].includes(step.type);
} }
/** /**
* 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中 * 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中
*/ */
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) { function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
if (parent && selectedKeys.value.includes(parent.stepId)) { if (parent && selectedKeys.value.includes(parent.id)) {
// //
selectedKeys.value.push(step.stepId); selectedKeys.value.push(step.id);
} }
} }
@ -425,7 +382,8 @@
]; ];
function setStepMoreAction(items: ActionsItem[], node: MsTreeNodeData) { function setStepMoreAction(items: ActionsItem[], node: MsTreeNodeData) {
if ((node as ScenarioStepItem).type === ScenarioStepType.CUSTOM_API) { const _stepType = getStepType(node as ScenarioStepItem);
if ((node as ScenarioStepItem).stepType === ScenarioStepType.CUSTOM_REQUEST) {
// //
return [ return [
{ {
@ -443,7 +401,7 @@
}, },
]; ];
} }
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_SCENARIO) { if (_stepType.isQuoteScenario) {
return [ return [
{ {
label: 'common.copy', label: 'common.copy',
@ -460,7 +418,7 @@
}, },
]; ];
} }
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_CASE) { if (_stepType.isQuoteCase) {
return [ return [
{ {
label: 'common.copy', label: 'common.copy',
@ -486,30 +444,30 @@
const id = getGenerateId(); const id = getGenerateId();
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
steps.value, steps.value,
node.stepId, node.id,
{ {
...cloneDeep( ...cloneDeep(
mapTree<ScenarioStepItem>(node, (childNode) => { mapTree<ScenarioStepItem>(node, (childNode) => {
return { return {
...childNode, ...childNode,
stepId: getGenerateId(), // TODO: ID id: getGenerateId(), // TODO: ID
}; };
})[0] })[0]
), ),
name: `copy-${node.name}`, name: `copy-${node.name}`,
order: node.order + 1, sort: node.sort + 1,
stepId: id, id,
}, },
'after', 'after',
checkedIfNeed, checkedIfNeed,
'stepId' 'id'
); );
break; break;
case 'config': case 'config':
console.log('config', node); console.log('config', node);
break; break;
case 'delete': case 'delete':
deleteNode(steps.value, node.stepId, 'stepId'); deleteNode(steps.value, node.id, 'id');
break; break;
default: default:
break; break;
@ -527,7 +485,7 @@
const tempStepName = ref(''); const tempStepName = ref('');
function handleStepNameClick(step: ScenarioStepItem) { function handleStepNameClick(step: ScenarioStepItem) {
tempStepName.value = step.name; tempStepName.value = step.name;
showStepNameEditInputStepId.value = step.stepId; showStepNameEditInputStepId.value = step.id;
nextTick(() => { nextTick(() => {
// //
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement; const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -536,7 +494,7 @@
} }
function applyStepNameChange(step: ScenarioStepItem) { function applyStepNameChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) { if (realStep) {
realStep.name = tempStepName.value; realStep.name = tempStepName.value;
} }
@ -549,8 +507,8 @@
const showStepDescEditInputStepId = ref<string | number>(''); const showStepDescEditInputStepId = ref<string | number>('');
const tempStepDesc = ref(''); const tempStepDesc = ref('');
function handleStepDescClick(step: ScenarioStepItem) { function handleStepDescClick(step: ScenarioStepItem) {
tempStepDesc.value = step.description; tempStepDesc.value = step.name;
showStepDescEditInputStepId.value = step.stepId; showStepDescEditInputStepId.value = step.id;
nextTick(() => { nextTick(() => {
// //
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement; const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -559,19 +517,21 @@
} }
function applyStepDescChange(step: ScenarioStepItem) { function applyStepDescChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) { if (realStep) {
realStep.description = tempStepDesc.value; realStep.name = tempStepDesc.value;
} }
showStepDescEditInputStepId.value = ''; showStepDescEditInputStepId.value = '';
} }
function handleStepContentChange($event, step: ScenarioStepItem) { function handleStepContentChange($event, step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) { if (realStep) {
Object.keys($event).forEach((key) => { // Object.keys($event).forEach((key) => {
realStep[key] = $event[key]; // realStep.config[key] = $event[key];
}); // });
realStep.config = $event;
console.log('handleStepContentChange', $event);
} }
} }
@ -579,12 +539,19 @@
* 处理步骤展开折叠 * 处理步骤展开折叠
*/ */
function handleStepExpand(data: MsTreeExpandedData) { function handleStepExpand(data: MsTreeExpandedData) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.id, 'id');
if (realStep) { if (realStep) {
realStep.expanded = !realStep.expanded; realStep.expanded = !realStep.expanded;
} }
} }
function handleStepToggleEnable(data: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.id, 'id');
if (realStep) {
realStep.enable = !realStep.enable;
}
}
const importApiDrawerVisible = ref(false); const importApiDrawerVisible = ref(false);
const customCaseDrawerVisible = ref(false); const customCaseDrawerVisible = ref(false);
const customApiDrawerVisible = ref(false); const customApiDrawerVisible = ref(false);
@ -594,7 +561,7 @@
const currentStepDetail = computed<any>(() => { const currentStepDetail = computed<any>(() => {
// TODO: // TODO:
if (activeStep.value) { if (activeStep.value) {
return stepsDetailMap.value[activeStep.value.stepId]; return stepDetails.value[activeStep.value.id];
} }
return undefined; return undefined;
}); });
@ -605,19 +572,20 @@
* @param step 点击的步骤节点 * @param step 点击的步骤节点
*/ */
function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) { function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
const _stepType = getStepType(step);
const offspringIds: string[] = []; const offspringIds: string[] = [];
mapTree(step.children || [], (e) => { mapTree(step.children || [], (e) => {
offspringIds.push(e.stepId); offspringIds.push(e.id);
return e; return e;
}); });
selectedKeys.value = [step.stepId, ...offspringIds]; selectedKeys.value = [step.id, ...offspringIds];
if ([ScenarioStepType.CUSTOM_API, ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(step.type)) { if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
activeStep.value = step; activeStep.value = step;
customApiDrawerVisible.value = true; customApiDrawerVisible.value = true;
} else if ([ScenarioStepType.QUOTE_CASE, ScenarioStepType.COPY_CASE].includes(step.type)) { } else if ([ScenarioStepType.QUOTE_CASE, ScenarioStepType.COPY_CASE].includes(step.type)) {
activeStep.value = step; activeStep.value = step;
customCaseDrawerVisible.value = true; customCaseDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) { } else if (step.stepType === ScenarioStepType.SCRIPT) {
activeStep.value = step; activeStep.value = step;
scriptOperationDrawerVisible.value = true; scriptOperationDrawerVisible.value = true;
} }
@ -663,39 +631,46 @@
* @param data 导入数据 * @param data 导入数据
*/ */
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) { function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
let order = steps.value.length + 1; let sort = steps.value.length + 1;
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
switch (activeCreateAction.value) { switch (activeCreateAction.value) {
case 'inside': case 'inside':
order = activeStep.value.children ? activeStep.value.children.length : 0; sort = activeStep.value.children ? activeStep.value.children.length : 0;
break; break;
case 'before': case 'before':
order = activeStep.value.order; sort = activeStep.value.sort;
break; break;
case 'after': case 'after':
order = activeStep.value.order + 1; sort = activeStep.value.sort + 1;
break; break;
default: default:
break; break;
} }
} }
const refType = type === 'copy' ? ScenarioStepRefType.COPY : ScenarioStepRefType.REF;
const insertApiSteps = buildInsertStepInfos( const insertApiSteps = buildInsertStepInfos(
data.api, data.api,
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API, ScenarioStepType.API,
order, refType,
stepsDetailMap.value sort,
stepDetails.value,
appStore.currentProjectId
); );
const insertCaseSteps = buildInsertStepInfos( const insertCaseSteps = buildInsertStepInfos(
data.case, data.case,
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE, ScenarioStepType.API_CASE,
order + insertApiSteps.length, refType,
stepsDetailMap.value sort + insertApiSteps.length,
stepDetails.value,
appStore.currentProjectId
); );
const insertScenarioSteps = buildInsertStepInfos( const insertScenarioSteps = buildInsertStepInfos(
data.scenario, data.scenario,
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO, ScenarioStepType.API_SCENARIO,
order + insertApiSteps.length + insertCaseSteps.length, refType,
stepsDetailMap.value sort + insertApiSteps.length + insertCaseSteps.length,
stepDetails.value,
appStore.currentProjectId
); );
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps); const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
@ -709,14 +684,17 @@
* 添加自定义 API 步骤 * 添加自定义 API 步骤
*/ */
function addCustomApiStep(request: RequestParam) { function addCustomApiStep(request: RequestParam) {
const id = getGenerateId(); request.isNew = false;
stepsDetailMap.value[id] = request; stepDetails.value[request.id] = request;
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.CUSTOM_API, stepType: ScenarioStepType.CUSTOM_REQUEST,
name: t('apiScenario.customApi'), name: t('apiScenario.customApi'),
} as ScenarioStepItem, method: request.method,
id: request.id,
projectId: appStore.currentProjectId,
},
activeStep.value, activeStep.value,
steps.value, steps.value,
activeCreateAction.value, activeCreateAction.value,
@ -725,11 +703,14 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: id, id: request.id,
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.CUSTOM_API, stepType: ScenarioStepType.CUSTOM_REQUEST,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.customApi'), name: t('apiScenario.customApi'),
} as ScenarioStepItem); method: request.method,
projectId: appStore.currentProjectId,
});
} }
} }
@ -738,7 +719,7 @@
*/ */
function applyApiStep(request: RequestParam | CaseRequestParam) { function applyApiStep(request: RequestParam | CaseRequestParam) {
if (activeStep.value) { if (activeStep.value) {
stepsDetailMap.value[activeStep.value?.stepId] = request; stepDetails.value[activeStep.value?.id] = request;
activeStep.value = undefined; activeStep.value = undefined;
} }
} }
@ -760,13 +741,14 @@
*/ */
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) { function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
const id = getGenerateId(); const id = getGenerateId();
stepsDetailMap.value[id] = cloneDeep(scriptProcessor); stepDetails.value[id] = cloneDeep(scriptProcessor);
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.SCRIPT_OPERATION, stepType: ScenarioStepType.SCRIPT,
name, name,
} as ScenarioStepItem, projectId: appStore.currentProjectId,
},
activeStep.value, activeStep.value,
steps.value, steps.value,
activeCreateAction.value, activeCreateAction.value,
@ -775,11 +757,13 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
stepId: id, id,
order: steps.value.length + 1, sort: steps.value.length + 1,
type: ScenarioStepType.SCRIPT_OPERATION, stepType: ScenarioStepType.SCRIPT,
refType: ScenarioStepRefType.DIRECT,
name, name,
} as ScenarioStepItem); projectId: appStore.currentProjectId,
});
} }
} }
@ -789,10 +773,10 @@
*/ */
function isAllowDropInside(dropNode: MsTreeNodeData) { function isAllowDropInside(dropNode: MsTreeNodeData) {
return [ return [
ScenarioStepType.LOOP_CONTROL, ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.CONDITION_CONTROL, ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONLY_ONCE_CONTROL, ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(dropNode.type); ].includes(dropNode.stepType);
} }
/** /**
@ -816,20 +800,20 @@
loading.value = true; loading.value = true;
const offspringIds: string[] = []; const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => { mapTree(dragNode.children || [], (e) => {
offspringIds.push(e.stepId); offspringIds.push(e.id);
return e; return e;
}); });
const stepIdAndOffspringIds = [dragNode.stepId, ...offspringIds]; const stepIdAndOffspringIds = [dragNode.id, ...offspringIds];
if (dropPosition === 0) { if (dropPosition === 0) {
// //
if (selectedKeys.value.includes(dropNode.stepId)) { if (selectedKeys.value.includes(dropNode.id)) {
// //
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds); selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
} }
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.stepId)) { } else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
// //
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds); selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.stepId)) { } else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
// //
selectedKeys.value = selectedKeys.value.filter((e) => { selectedKeys.value = selectedKeys.value.filter((e) => {
for (let i = 0; i < stepIdAndOffspringIds.length; i++) { for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
@ -842,7 +826,7 @@
return true; return true;
}); });
} }
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'stepId'); const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
if (dragResult) { if (dragResult) {
Message.success(t('common.moveSuccess')); Message.success(t('common.moveSuccess'));
} }
@ -861,12 +845,12 @@
const quickInputDataKey = ref(''); const quickInputDataKey = ref('');
function setQuickInput(step: ScenarioStepItem, dataKey: string) { function setQuickInput(step: ScenarioStepItem, dataKey: string) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) { if (realStep) {
activeStep.value = realStep as ScenarioStepItem; activeStep.value = realStep as ScenarioStepItem;
} }
quickInputDataKey.value = dataKey; quickInputDataKey.value = dataKey;
quickInputParamValue.value = step.variableVal; quickInputParamValue.value = step.config?.[dataKey] || '';
showQuickInput.value = true; showQuickInput.value = true;
} }

View File

@ -3,19 +3,32 @@
<template #first> <template #first>
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load> <a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]"> <a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]">
<step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:step="scenario.stepInfo" is-new /> <step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:scenario="scenario" is-new />
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PARAMS" :title="t('apiScenario.params')" class="p-[16px]"> <a-tab-pane :key="ScenarioCreateComposition.PARAMS" :title="t('apiScenario.params')" class="p-[16px]">
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="scenario.params" /> <params
v-if="activeKey === ScenarioCreateComposition.PARAMS"
v-model:params="scenario.scenarioConfig.variable.commonVariables"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PRE_POST" :title="t('apiScenario.prePost')" class="p-[16px]"> <a-tab-pane :key="ScenarioCreateComposition.PRE_POST" :title="t('apiScenario.prePost')" class="p-[16px]">
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" /> <prePost
v-if="activeKey === ScenarioCreateComposition.PRE_POST"
v-model:post-processor-config="scenario.scenarioConfig.postProcessorConfig"
v-model:pre-processor-config="scenario.scenarioConfig.preProcessorConfig"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.ASSERTION" :title="t('apiScenario.assertion')" class="p-[16px]"> <a-tab-pane :key="ScenarioCreateComposition.ASSERTION" :title="t('apiScenario.assertion')" class="p-[16px]">
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" /> <assertion
v-if="activeKey === ScenarioCreateComposition.ASSERTION"
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="p-[16px]"> <a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="p-[16px]">
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" /> <setting
v-if="activeKey === ScenarioCreateComposition.SETTING"
v-model:other-config="scenario.scenarioConfig.otherConfig"
/>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</template> </template>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="h-full w-full overflow-hidden"> <div class="h-full w-full overflow-hidden">
<div class="px-[24px] pt-[16px]"> <div class="px-[24px] pt-[16px]">
<MsDetailCard :title="`【${previewDetail.num}】${previewDetail.name}`" :description="description"> <MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description">
<template #titleAppend> <template #titleAppend>
<apiStatus :status="previewDetail.status" size="small" /> <apiStatus :status="scenario.status" size="small" />
</template> </template>
<template #titleRight> <template #titleRight>
<a-button <a-button
@ -15,11 +15,11 @@
> >
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<MsIcon <MsIcon
:type="previewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'" :type="scenario.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${previewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`" :class="`${scenario.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
:size="14" :size="14"
/> />
{{ t(previewDetail.follow ? 'common.forked' : 'common.fork') }} {{ t(scenario.follow ? 'common.forked' : 'common.fork') }}
</div> </div>
</a-button> </a-button>
<a-button type="outline" size="mini" class="arco-btn-outline--secondary !bg-transparent" @click="share"> <a-button type="outline" size="mini" class="arco-btn-outline--secondary !bg-transparent" @click="share">
@ -29,8 +29,8 @@
</div> </div>
</a-button> </a-button>
</template> </template>
<template #type="{ value }"> <template #priority="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag /> <caseLevel :case-level="value as CaseLevel" />
</template> </template>
</MsDetailCard> </MsDetailCard>
</div> </div>
@ -41,50 +41,57 @@
:title="t('apiScenario.baseInfo')" :title="t('apiScenario.baseInfo')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
BASE_INFO <baseInfo :scenario="scenario as ScenarioDetail" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]"> <a-tab-pane :key="ScenarioDetailComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]">
<step v-if="activeKey === ScenarioCreateComposition.STEP" :step="previewDetail.step" /> <step v-if="activeKey === ScenarioDetailComposition.STEP" v-model:scenario="scenario" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane <a-tab-pane
:key="ScenarioCreateComposition.PARAMS" :key="ScenarioDetailComposition.PARAMS"
:title="t('apiScenario.params')" :title="t('apiScenario.params')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="allParams" /> <params
v-if="activeKey === ScenarioDetailComposition.PARAMS"
v-model:params="scenario.scenarioConfig.variable.commonVariables"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane <a-tab-pane
:key="ScenarioCreateComposition.PRE_POST" :key="ScenarioDetailComposition.PRE_POST"
:title="t('apiScenario.prePost')" :title="t('apiScenario.prePost')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" /> <prePost
v-if="activeKey === ScenarioDetailComposition.PRE_POST"
v-model:post-processor-config="scenario.scenarioConfig.postProcessorConfig"
v-model:pre-processor-config="scenario.scenarioConfig.preProcessorConfig"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane <a-tab-pane
:key="ScenarioCreateComposition.ASSERTION" :key="ScenarioDetailComposition.ASSERTION"
:title="t('apiScenario.assertion')" :title="t('apiScenario.assertion')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" /> <assertion
v-if="activeKey === ScenarioDetailComposition.ASSERTION"
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane <a-tab-pane
:key="ScenarioDetailComposition.EXECUTE_HISTORY" :key="ScenarioDetailComposition.EXECUTE_HISTORY"
:title="t('apiScenario.executeHistory')" :title="t('apiScenario.executeHistory')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
<executeHistory <executeHistory v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY" :scenario-id="scenario.id" />
v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY"
:scenario-id="previewDetail.id"
/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane <a-tab-pane
:key="ScenarioDetailComposition.CHANGE_HISTORY" :key="ScenarioDetailComposition.CHANGE_HISTORY"
:title="t('apiScenario.changeHistory')" :title="t('apiScenario.changeHistory')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
> >
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" :source-id="previewDetail.id" /> <changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" :source-id="scenario.id" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane <!-- <a-tab-pane
:key="ScenarioDetailComposition.DEPENDENCY" :key="ScenarioDetailComposition.DEPENDENCY"
:title="t('apiScenario.dependency')" :title="t('apiScenario.dependency')"
class="px-[24px] py-[16px]" class="px-[24px] py-[16px]"
@ -93,9 +100,12 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane :key="ScenarioDetailComposition.QUOTE" :title="t('apiScenario.quote')" class="px-[24px] py-[16px]"> <a-tab-pane :key="ScenarioDetailComposition.QUOTE" :title="t('apiScenario.quote')" class="px-[24px] py-[16px]">
<quote v-if="activeKey === ScenarioDetailComposition.QUOTE" /> <quote v-if="activeKey === ScenarioDetailComposition.QUOTE" />
</a-tab-pane> </a-tab-pane> -->
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="px-[24px] py-[16px]"> <a-tab-pane :key="ScenarioDetailComposition.SETTING" :title="t('common.setting')" class="px-[24px] py-[16px]">
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" /> <setting
v-if="activeKey === ScenarioDetailComposition.SETTING"
v-model:other-config="scenario.scenarioConfig.otherConfig"
/>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
@ -106,47 +116,52 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue'; import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import baseInfo from '../components/baseInfo.vue';
import step from '../components/step/index.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { RequestMethods, ScenarioCreateComposition, ScenarioDetailComposition } from '@/enums/apiEnum'; import { Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
import { ScenarioDetailComposition } from '@/enums/apiEnum';
// //
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
const params = defineAsyncComponent(() => import('../components/params.vue')); const params = defineAsyncComponent(() => import('../components/params.vue'));
const prePost = defineAsyncComponent(() => import('../components/prePost.vue')); const prePost = defineAsyncComponent(() => import('../components/prePost.vue'));
const assertion = defineAsyncComponent(() => import('../components/assertion.vue')); const assertion = defineAsyncComponent(() => import('../components/assertion.vue'));
const executeHistory = defineAsyncComponent(() => import('../components/executeHistory.vue')); const executeHistory = defineAsyncComponent(() => import('../components/executeHistory.vue'));
const changeHistory = defineAsyncComponent(() => import('../components/changeHistory.vue')); const changeHistory = defineAsyncComponent(() => import('../components/changeHistory.vue'));
const dependency = defineAsyncComponent(() => import('../components/dependency.vue')); // const dependency = defineAsyncComponent(() => import('../components/dependency.vue'));
const quote = defineAsyncComponent(() => import('../components/quote.vue')); // const quote = defineAsyncComponent(() => import('../components/quote.vue'));
const setting = defineAsyncComponent(() => import('../components/setting.vue')); const setting = defineAsyncComponent(() => import('../components/setting.vue'));
const allParams = ref<any[]>([]);
const props = defineProps<{
detail: Record<string, any>;
}>();
const emit = defineEmits(['updateFollow']); const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
const { t } = useI18n(); const { t } = useI18n();
const previewDetail = ref<Record<string, any>>(cloneDeep(props.detail)); const scenario = defineModel<Scenario>('scenario', {
required: true,
});
const description = computed(() => [ const description = computed(() => [
{ {
key: 'type', key: 'priority',
locale: 'something.type', locale: 'apiScenario.scenarioLevel',
value: 'type', value: scenario.value.priority,
}, },
{ {
key: 'path', key: 'tag',
locale: 'something.path', locale: 'common.tag',
value: 'path', value: scenario.value.tags,
},
{
key: 'description',
locale: 'common.desc',
value: scenario.value.description,
}, },
]); ]);
@ -154,7 +169,7 @@
async function toggleFollowReview() { async function toggleFollowReview() {
try { try {
followLoading.value = true; followLoading.value = true;
Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess')); Message.success(scenario.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
emit('updateFollow'); emit('updateFollow');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -166,17 +181,20 @@
function share() { function share() {
if (isSupported) { if (isSupported) {
copy(`${window.location.href}&dId=${previewDetail.value.id}`); copy(`${window.location.href}&dId=${scenario.value.id}`);
Message.success(t('common.copySuccess')); Message.success(t('common.copySuccess'));
} else { } else {
Message.error(t('common.copyNotSupport')); Message.error(t('common.copyNotSupport'));
} }
} }
const activeKey = ref<ScenarioCreateComposition | ScenarioDetailComposition>(ScenarioDetailComposition.BASE_INFO); const activeKey = ref<ScenarioDetailComposition>(ScenarioDetailComposition.STEP);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.arco-tabs-nav) {
@apply border-b;
}
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
@apply pt-0; @apply pt-0;
} }

View File

@ -5,7 +5,7 @@
v-model:active-tab="activeScenarioTab" v-model:active-tab="activeScenarioTab"
v-model:tabs="apiTabs" v-model:tabs="apiTabs"
class="flex-1 overflow-hidden" class="flex-1 overflow-hidden"
@add="newTab" @add="() => newTab()"
> >
<template #label="{ tab }"> <template #label="{ tab }">
<a-tooltip :content="tab.label" :mouse-enter-delay="500"> <a-tooltip :content="tab.label" :mouse-enter-delay="500">
@ -34,7 +34,7 @@
:is-show-scenario="isShowScenario" :is-show-scenario="isShowScenario"
@folder-node-select="handleNodeSelect" @folder-node-select="handleNodeSelect"
@init="handleModuleInit" @init="handleModuleInit"
@new-scenario="newTab" @new-scenario="() => newTab()"
></scenarioModuleTree> ></scenarioModuleTree>
</div> </div>
<div class="flex-1"> <div class="flex-1">
@ -55,6 +55,7 @@
:active-module="activeModule" :active-module="activeModule"
:offspring-ids="offspringIds" :offspring-ids="offspringIds"
@refresh-module-tree="refreshTree" @refresh-module-tree="refreshTree"
@open-scenario="openScenarioTab"
/> />
</template> </template>
</MsSplitBox> </MsSplitBox>
@ -63,7 +64,7 @@
<create v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create> <create v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
</div> </div>
<div v-else class="pageWrap"> <div v-else class="pageWrap">
<detail :detail="activeScenarioTab"></detail> <detail v-model:scenario="activeScenarioTab"></detail>
</div> </div>
</MsCard> </MsCard>
</template> </template>
@ -74,66 +75,69 @@
*/ */
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue'; import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import scenarioModuleTree from './components/scenarioModuleTree.vue'; import scenarioModuleTree from './components/scenarioModuleTree.vue';
import { ScenarioStepInfo } from './components/step/index.vue';
import environmentSelect from '@/views/api-test/components/environmentSelect.vue'; import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
// import executeButton from '@/views/api-test/components/executeButton.vue'; // import executeButton from '@/views/api-test/components/executeButton.vue';
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue'; import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
import { getTrashModuleCount } from '@/api/modules/api-test/scenario'; import { addScenario, getScenarioDetail, getTrashModuleCount, updateScenario } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import router from '@/router'; import router from '@/router';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { getGenerateId, mapTree, TreeNode } from '@/utils';
import { ApiScenarioGetModuleParams, Scenario } from '@/models/apiTest/scenario'; import {
ApiScenarioGetModuleParams,
ApiScenarioTableItem,
Scenario,
ScenarioStepItem,
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { RequestDefinitionStatus } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultScenario } from './components/config';
// //
const detail = defineAsyncComponent(() => import('./detail/index.vue')); const detail = defineAsyncComponent(() => import('./detail/index.vue'));
const create = defineAsyncComponent(() => import('./create/index.vue')); const create = defineAsyncComponent(() => import('./create/index.vue'));
export type ScenarioParams = Scenario & TabItem;
const { t } = useI18n(); const { t } = useI18n();
const apiTabs = ref<Scenario[]>([ const apiTabs = ref<ScenarioParams[]>([
{ {
id: 'all', id: 'all',
label: t('apiScenario.allScenario'), label: t('apiScenario.allScenario'),
closable: false, closable: false,
} as Scenario, } as ScenarioParams,
]); ]);
const activeScenarioTab = ref<Scenario>(apiTabs.value[0]); const activeScenarioTab = ref<ScenarioParams>(apiTabs.value[0] as ScenarioParams);
function newTab() { function newTab(defaultScenarioInfo?: Scenario, isCopy = false) {
apiTabs.value.push({ if (defaultScenarioInfo) {
id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`, apiTabs.value.push({
label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`, ...defaultScenarioInfo,
closable: true, id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
isNew: true, label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
name: '', });
moduleId: 'root', } else {
priority: 'P0', apiTabs.value.push({
stepInfo: { ...cloneDeep(defaultScenario),
id: new Date().getTime(), id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
steps: [], label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
executeTime: '', moduleId: 'root',
executeSuccessCount: 0, priority: 'P0',
executeFailCount: 0, });
stepsDetailMap: {}, }
} as ScenarioStepInfo, activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1] as ScenarioParams;
status: RequestDefinitionStatus.PROCESSING,
tags: [],
params: [],
executeLoading: false,
unSaved: false,
});
activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1];
} }
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
@ -185,16 +189,49 @@
const saveLoading = ref(false); const saveLoading = ref(false);
async function saveScenario() { async function saveScenario() {
saveLoading.value = true; try {
await new Promise((resolve) => { saveLoading.value = true;
setTimeout(() => { if (activeScenarioTab.value.isNew) {
resolve(''); const res = await addScenario({
}, 1000); ...activeScenarioTab.value,
}); projectId: appStore.currentProjectId,
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess')); });
activeScenarioTab.value.isNew = false; const scenarioDetail = await getScenarioDetail(res.id);
activeScenarioTab.value.unSaved = false; activeScenarioTab.value = scenarioDetail as ScenarioParams;
saveLoading.value = false; } else {
await updateScenario({
...activeScenarioTab.value,
});
}
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess'));
activeScenarioTab.value.unSaved = false;
saveLoading.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveLoading.value = false;
}
}
async function openScenarioTab(record: ApiScenarioTableItem, isCopy?: boolean) {
try {
appStore.showLoading();
const res = await getScenarioDetail(record.id);
res.stepDetails = {};
// mapTree<ScenarioStepItem>(res.steps, (node: TreeNode<ScenarioStepItem>) => {
// res.stepDetails[node.id] = node.config;
// return node;
// });
newTab(res, isCopy);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
nextTick(() => {
appStore.hideLoading();
});
}
} }
</script> </script>

View File

@ -110,12 +110,14 @@ export default {
'apiScenario.after': '在之后插入步骤', 'apiScenario.after': '在之后插入步骤',
'apiScenario.num': '次数', 'apiScenario.num': '次数',
'apiScenario.space': '间隔(ms)', 'apiScenario.space': '间隔(ms)',
'apiScenario.overTime': '超时(ms)', 'apiScenario.timeout': '超时(ms)',
'apiScenario.waitTimeMs': '等待(ms)', 'apiScenario.waitTimeMs': '等待(ms)',
'apiScenario.pleaseInputStepDesc': '请输入步骤描述', 'apiScenario.pleaseInputStepDesc': '请输入步骤描述',
'apiScenario.variableName': '变量名称{suffix}', 'apiScenario.variable': '变量名称{suffix}',
'apiScenario.variablePrefix': '变量前缀', 'apiScenario.valuePrefix': '变量前缀',
'apiScenario.variableVal': '变量值', 'apiScenario.value': '变量值',
'apiScenario.whileController.msWhileVariable.value': '变量值',
'apiScenario.whileController.msWhileScript.scriptValue': '表达式',
'apiScenario.condition': '条件', 'apiScenario.condition': '条件',
'apiScenario.expression': '表达式', 'apiScenario.expression': '表达式',
'apiScenario.equal': '等于', 'apiScenario.equal': '等于',

View File

@ -43,7 +43,7 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
<detail v-else :detail="activeApiTab"></detail> <detail v-else v-model:scenario="activeApiTab"></detail>
</MsCard> </MsCard>
</template> </template>