feat(项目管理): 环境管理&模块&前后置组件扩展和参数对接

This commit is contained in:
xinxin.wu 2024-03-06 19:39:39 +08:00 committed by Craftsman
parent a2254a9c44
commit b362fd6a0c
21 changed files with 585 additions and 463 deletions

View File

@ -424,7 +424,7 @@
const responseRadios = [
{ label: 'ms.assertion.jsonPath', value: 'jsonPath' },
{ label: 'ms.assertion.xpath', value: 'xPath' },
{ label: 'ms.assertion.document', value: 'document' },
// { label: 'ms.assertion.document', value: 'document' },
{ label: 'ms.assertion.regular', value: 'regular' },
{ label: 'ms.assertion.script', value: 'script' },
];

View File

@ -246,6 +246,9 @@ export interface ExecuteConditionProcessorCommon {
enable: boolean; // 是否启用
name?: string; // 条件处理器名称
processorType: RequestConditionProcessor;
associateScenarioResult?: boolean; // 是否关联场景结果
ignoreProtocols: string[]; // 忽略协议
beforeStepScript: boolean; // 是否是步骤内前置脚本前
}
// 执行请求-前后置条件-脚本处理器
export type ScriptProcessor = ScriptCommonConfig & ExecuteConditionProcessorCommon;

View File

@ -101,7 +101,7 @@ export interface SelectedModule {
// 定义-获取环境的模块树参数
export interface ApiDefinitionGetEnvModuleParams {
projectId: string;
selectedModules: SelectedModule[];
selectedModules?: SelectedModule[];
}
// 环境-模块树
export interface EnvModule {

View File

@ -35,6 +35,9 @@ export interface ProcessorConfig {
scenarioProcessorConfig: {
processors: ExecuteConditionProcessor[];
};
requestProcessorConfig: {
processors: [];
};
};
}
export interface AssertionConfig {
@ -138,5 +141,16 @@ export interface HttpForm {
// pathMatchRule: {
path: string;
condition: string;
// };
moduleId: string[];
moduleMatchRule: {
modules: {
moduleId: string;
containChildModule: boolean;
}[];
};
url: string;
pathMatchRule: {
path: '';
condition: '';
};
}

View File

@ -240,6 +240,7 @@ const useAppStore = defineStore('app', {
async initSystemPackage() {
try {
this.packageType = await getPackageType();
// this.packageType = 'community';
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -30,18 +30,19 @@ const envParamsDefaultConfig: EnvConfig = {
scenarioProcessorConfig: {
processors: [],
},
requestProcessorConfig: {
processors: [],
},
},
// TODO 环境参数问题
// apiProcessorConfig: [],
},
postProcessorConfig: {
// TODO 环境参数问题
// apiProcessorConfig: [],
apiProcessorConfig: {
scenarioProcessorConfig: {
processors: [],
},
requestProcessorConfig: {
processors: [],
},
},
},
assertionConfig: { assertions: [] },

View File

@ -632,3 +632,32 @@ export function customFieldDataToTableData(customFieldData: Record<string, any>[
});
return tableData;
}
/**
* Id对应的name列表
*/
/**
* Id对应的name列表
* @param trees
* @param targetIds
*/
export function findNodeNames<T>(trees: TreeNode<T>[], targetIds: string[]) {
const result: string[] = [];
// eslint-disable-next-line no-shadow
function findNameRecursive(node: TreeNode<T>, targetIds: string[]) {
if (targetIds.includes(node.id)) {
result.push(node.name);
}
if (node.children) {
node.children.forEach((child) => {
findNameRecursive(child, targetIds);
});
}
}
trees.forEach((module) => {
findNameRecursive(module, targetIds);
});
return result;
}

View File

@ -2,10 +2,58 @@
<div class="condition-content">
<!-- 脚本操作 -->
<template v-if="condition.processorType === RequestConditionProcessor.SCRIPT">
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[8px]">
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio :value="true">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<!-- 前后置请求开始 -->
<div v-if="props.showPrePostRequest" class="mt-4">
<a-radio-group v-model="condition.beforeStepScript" type="button" size="small" :default-value="true">
<a-radio :value="true"> {{ props?.requestRadioTextProps?.pre }} </a-radio>
<a-radio :value="false"> {{ props?.requestRadioTextProps?.post }} </a-radio>
</a-radio-group>
<a-tooltip position="br" :content="t('apiTestDebug.preconditionAssociateResultDesc')">
<IconQuestionCircle class="ml-2 h-[16px] w-[16px] text-[--color-text-4] hover:text-[rgb(var(--primary-5))]" />
<template #content>
<div>{{ props?.requestRadioTextProps?.preTip }}</div>
<div>{{ props?.requestRadioTextProps?.postTip }}</div>
</template>
</a-tooltip>
</div>
<div v-if="props.showPrePostRequest" class="my-4">
<MsSelect
v-model:model-value="condition.ignoreProtocols"
:style="{ width: '332px' }"
:allow-search="false"
allow-clear
:options="protocolList"
:multiple="true"
:has-all-select="true"
value-key="protocol"
label-key="protocol"
:prefix="t('project.environmental.preOrPost.ignoreProtocols')"
>
</MsSelect>
</div>
<!-- 前后置请求结束 -->
<div class="flex items-center justify-between">
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[8px]">
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio :value="true">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div v-if="props.showAssociatedScene" class="flex items-center">
<a-switch
v-model="condition.associateScenarioResult"
class="mr-2"
size="small"
type="line"
@change="emit('change')"
/>
{{ t('apiTestDebug.preconditionAssociatedSceneResult') }}
<a-tooltip position="br" :content="t('apiTestDebug.preconditionAssociateResultDesc')">
<IconQuestionCircle
class="ml-2 h-[16px] w-[16px] text-[--color-text-4] hover:text-[rgb(var(--primary-5))]"
/>
</a-tooltip>
</div>
</div>
<div
v-if="!condition.enableCommonScript"
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)]"
@ -358,13 +406,17 @@
import InsertCommonScript from '@/components/business/ms-common-script/insertCommonScript.vue';
import AddScriptDrawer from '@/components/business/ms-common-script/ms-addScriptDrawer.vue';
import MsScriptDefined from '@/components/business/ms-common-script/scriptDefined.vue';
import MsSelect from '@/components/business/ms-select';
import fastExtraction from '../fastExtraction/index.vue';
import moreSetting from '../fastExtraction/moreSetting.vue';
import paramTable, { type ParamTableColumn } from '../paramTable.vue';
import quoteSqlSourceDrawer from '../quoteSqlSourceDrawer.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { ProtocolItem } from '@/models/apiTest/common';
import { ExecuteConditionProcessor, JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common';
import { ParamsRequestType } from '@/models/projectManagement/commonScript';
import {
@ -378,13 +430,22 @@
} from '@/enums/apiEnum';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{
data: ExecuteConditionProcessor;
response?: string; //
heightUsed?: number;
isBuildIn?: boolean; //
}>();
const appStore = useAppStore();
const props = withDefaults(
defineProps<{
data: ExecuteConditionProcessor;
response?: string; //
heightUsed?: number;
isBuildIn?: boolean; //
showAssociatedScene?: boolean; //
requestRadioTextProps?: Record<string, any>; //
showPrePostRequest?: boolean; //
}>(),
{
showAssociatedScene: false,
showPrePostRequest: false,
}
);
const emit = defineEmits<{
(e: 'update:data', data: ExecuteConditionProcessor): void;
(e: 'copy'): void;
@ -732,6 +793,15 @@ if (!result){
});
emit('change');
}
const protocolList = ref<ProtocolItem[]>([]);
onBeforeMount(async () => {
try {
protocolList.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
console.log(error);
}
});
</script>
<style lang="less" scoped>

View File

@ -30,6 +30,9 @@
v-model:data="activeItem"
:response="props.response"
:height-used="props.heightUsed"
:show-associated-scene="props.showAssociatedScene"
:show-pre-post-request="props.showPrePostRequest"
:request-radio-text-props="props.requestRadioTextProps"
@copy="copyListItem"
@delete="deleteListItem"
@change="emit('change')"
@ -48,13 +51,21 @@
import { ConditionType, ExecuteConditionProcessor } from '@/models/apiTest/common';
import { RequestConditionProcessor } from '@/enums/apiEnum';
const props = defineProps<{
list: ExecuteConditionProcessor[];
conditionTypes: Array<ConditionType>;
addText: string;
heightUsed?: number;
response?: string; //
}>();
const props = withDefaults(
defineProps<{
list: ExecuteConditionProcessor[];
conditionTypes: Array<ConditionType>;
addText: string;
requestRadioTextProps?: Record<string, any>;
heightUsed?: number;
response?: string; //
showAssociatedScene?: boolean;
showPrePostRequest?: boolean; //
}>(),
{
showAssociatedScene: false,
}
);
const emit = defineEmits<{
(e: 'update:list', list: ExecuteConditionProcessor[]): void;
(e: 'change'): void;
@ -107,6 +118,9 @@
processorType: RequestConditionProcessor.SCRIPT,
scriptName: t('apiTestDebug.preconditionScriptName'),
enableCommonScript: false,
associateScenarioResult: false,
ignoreProtocols: [],
beforeStepScript: true,
enable: true,
script: '',
scriptId: '',
@ -125,6 +139,9 @@
id,
processorType: RequestConditionProcessor.SQL,
enableCommonScript: false,
associateScenarioResult: false,
ignoreProtocols: [],
beforeStepScript: true,
description: '',
enable: true,
dataSourceId: '',
@ -141,6 +158,9 @@
data.value.push({
id,
processorType: RequestConditionProcessor.TIME_WAITING,
associateScenarioResult: false,
ignoreProtocols: [],
beforeStepScript: true,
enable: true,
delay: 1000,
});
@ -149,6 +169,10 @@
data.value.push({
id,
processorType: RequestConditionProcessor.EXTRACT,
enableCommonScript: false,
associateScenarioResult: false,
ignoreProtocols: [],
beforeStepScript: true,
enable: true,
extractors: [],
});

View File

@ -70,7 +70,10 @@ export default {
'apiTestDebug.wait': 'Wait',
'apiTestDebug.script': 'Script',
'apiTestDebug.preconditionScriptName': 'Pre-script name',
'apiTestDebug.preconditionAssociatedSceneResult': 'Associated scene result',
'apiTestDebug.preconditionScriptNamePlaceholder': 'Please enter the pre-script name',
'apiTestDebug.preconditionAssociateResultDesc':
'Counted in the scene execution result as a custom script step. Execution error will affect the final scene execution result',
'apiTestDebug.manual': 'Manual entry',
'apiTestDebug.quote': 'Quoting public scripts',
'apiTestDebug.commonScriptList': 'Public script list',

View File

@ -66,7 +66,10 @@ export default {
'apiTestDebug.wait': '等待',
'apiTestDebug.script': '脚本操作',
'apiTestDebug.preconditionScriptName': '前置脚本名称',
'apiTestDebug.preconditionAssociatedSceneResult': '关联场景结果',
'apiTestDebug.preconditionScriptNamePlaceholder': '请输入前置脚本名称',
'apiTestDebug.preconditionAssociateResultDesc':
'当作自定义脚本步骤统计到场景执行结果中,执行报错时会影响场景的最终执行结果',
'apiTestDebug.manual': '手动录入',
'apiTestDebug.quote': '引用公共脚本',
'apiTestDebug.commonScriptList': '公共脚本列表',

View File

@ -39,14 +39,20 @@
<a-tab-pane key="displaySetting" :title="t('project.environmental.displaySetting')" />
</a-tabs>
</div>
<a-divider :margin="0" class="!mb-[16px]" />
<a-divider
:margin="0"
:class="{
'!mb-[16px]': activeKey === 'pre' || activeKey === 'post' ? false : true,
}"
/>
<div class="content">
<EnvParamsTab v-if="activeKey === 'envParams'" />
<HttpTab v-else-if="activeKey === 'http'" />
<DataBaseTab v-else-if="activeKey === 'database'" />
<HostTab v-else-if="activeKey === 'host'" />
<PreTab v-else-if="activeKey === 'pre'" />
<PostTab v-else-if="activeKey === 'post'" />
<!-- <PreTab v-else-if="activeKey === 'pre'" />
<PostTab v-else-if="activeKey === 'post'" /> -->
<PreAndPostTab v-else-if="activeKey === 'pre' || activeKey === 'post'" :active-type="activeKey" />
<AssertTab v-else-if="activeKey === 'assert'" />
<template v-for="item in envPluginList" :key="item.pluginId">
<PluginTab
@ -80,6 +86,7 @@
import HttpTab from './envParams/HttpTab.vue';
import PluginTab from './envParams/PluginTab.vue';
import PostTab from './envParams/PostTab.vue';
import PreAndPostTab from './envParams/preAndPost.vue';
import PreTab from './envParams/PreTab.vue';
import { getEnvPlugin, updateOrAddEnv } from '@/api/modules/project-management/envManagement';

View File

@ -4,34 +4,64 @@
<a-switch v-model:model-value="currentList.enable" type="line" size="small" />
<div class="text-[var(--color-text-1)]">{{ t('project.environmental.host.config') }}</div>
</div>
<div class="mt-[8px]">
<!-- <a-radio-group v-model:model-value="addType" class="mt-[16px]" size="small" type="button">
<a-radio value="single">单个添加</a-radio>
<a-radio value="multiple">批量添加</a-radio>
</a-radio-group> -->
<div v-show="addType === 'single'" class="mt-[8px]">
<MsBatchForm
ref="batchFormRef"
:models="batchFormModels"
:form-mode="ruleFormMode"
add-text="project.menu.rule.addRule"
:default-vals="currentList.hosts"
show-enable
:show-enable="false"
:is-show-drag="false"
></MsBatchForm>
</div>
<!-- <div v-show="addType === 'multiple'">
<MsCodeEditor
v-model:model-value="editorContent"
width="100%"
height="250px"
theme="MS-text"
:show-theme-change="false"
>
<template #leftTitle>
<a-form-item
:label="t('system.resourcePool.batchAddResource')"
asterisk-position="end"
class="hide-wrapper mb-0 w-auto"
required
>
</a-form-item>
</template>
</MsCodeEditor>
<div class="mb-[24px] text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('system.resourcePool.nodeConfigEditorTip') }}
</div>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { onClickOutside } from '@vueuse/core';
import { debounce } from 'lodash-es';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
import { FormItemModel } from '@/components/business/ms-batch-form/types';
import { useI18n } from '@/hooks/useI18n';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { getGenerateId } from '@/utils';
import { EnvConfigItem } from '@/models/projectManagement/environmental';
const { t } = useI18n();
const store = useProjectEnvStore();
const addType = ref<string>('single');
const currentList = computed({
get: () => store.currentEnvDetailInfo.config.hostConfig || {},
@ -56,20 +86,58 @@
],
},
{
filed: 'hostName',
filed: 'domain',
type: 'input',
label: 'project.environmental.host.hostName',
placeholder: 'project.environmental.host.hostNamePlaceholder',
rules: [{ required: true, message: t('project.environmental.host.hostNameIsRequire') }],
},
{
filed: 'desc',
filed: 'description',
type: 'input',
label: 'project.environmental.host.desc',
placeholder: 'project.environmental.host.descPlaceholder',
},
]);
const ruleFormMode = ref<UserModalMode>('create');
//
const editorContent = ref('');
/**
* 解析代码编辑器内容
*/
function analyzeCode() {
const arr = editorContent.value.replaceAll('\r', '\n').split('\n'); //
//
arr.forEach((e, i) => {
if (e.trim() !== '') {
//
const line = e.split(',');
if (line.every((s) => s.trim() !== '')) {
const item = {
ip: line[0],
domain: line[1],
};
if (i === 0) {
// concurrentNumber
currentList.value = [item];
} else {
currentList.value.push(item);
}
}
}
});
}
watch(
() => editorContent.value,
(val) => {
if (val) {
debounce(analyzeCode, 300);
}
}
);
</script>
<style lang="less" scoped></style>

View File

@ -18,20 +18,26 @@
<span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.linkTimeOut') }}</span>
</template>
</a-input-number>
<a-select v-model:model-value="form.authType" class="w-[200px]">
<!-- TOTO 第一个版本不做 -->
<!-- <a-select v-model:model-value="form.authType" class="w-[200px]">
<template #prefix>
<span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.authType') }}</span>
</template>
<a-option>Basic Auth</a-option>
<a-option>Basic Auth2</a-option>
<a-option>Basic Auth3</a-option>
</a-select>
</a-select> -->
</div>
</div>
<MsBaseTable class="mt-[16px]" v-bind="propsRes" v-on="propsEvent" @change="changeHandler">
<template #type="{ record }">
<span>{{ getEnableScope(record.type) }}</span>
</template>
<template #moduleValue="{ record }">
<a-tooltip :content="getModuleName(record)" position="left">
<span class="one-line-text max-w-[300px]">{{ getModuleName(record) }}</span>
</a-tooltip>
</template>
<template #operation="{ record }">
<div class="flex flex-row flex-nowrap items-center">
<MsButton class="!mr-0" @click="handleCopy(record)">{{ t('common.copy') }}</MsButton>
@ -42,7 +48,13 @@
</div>
</template>
</MsBaseTable>
<AddHttpDrawer v-model:visible="addVisible" :is-copy="isCopy" :current-id="httpId" @close="addVisible = false" />
<AddHttpDrawer
v-model:visible="addVisible"
:module-tree="moduleTree"
:is-copy="isCopy"
:current-id="httpId"
@close="addVisible = false"
/>
</template>
<script lang="ts" async setup>
@ -56,15 +68,19 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import AddHttpDrawer from './popUp/AddHttpDrawer.vue';
import { getEnvModules } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import { useAppStore, useTableStore } from '@/store';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { findNodeNames } from '@/utils';
import { BugListItem } from '@/models/bug-management';
import type { ModuleTreeNode } from '@/models/common';
import type { CommonParams } from '@/models/projectManagement/environmental';
import { HttpForm } from '@/models/projectManagement/environmental';
import { TableKeyEnum } from '@/enums/tableEnum';
const appStore = useAppStore();
const { t } = useI18n();
const store = useProjectEnvStore();
@ -97,7 +113,8 @@
{
title: 'project.environmental.http.value',
dataIndex: 'value',
showTooltip: true,
slotName: 'moduleValue',
showTooltip: false,
showDrag: true,
showInTable: true,
},
@ -223,6 +240,27 @@
break;
}
}
const moduleTree = ref<ModuleTreeNode[]>([]);
async function initModuleTree() {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
});
moduleTree.value = res.moduleTree;
} catch (error) {
console.log(error);
}
}
await initModuleTree();
function getModuleName(record: HttpForm) {
if (record.type === 'MODULE') {
const moduleIds: string[] = record.moduleMatchRule.modules.map((item) => item.moduleId);
const result = findNodeNames(moduleTree.value, moduleIds);
return result.join(',');
}
return '-';
}
</script>
<style lang="less" scoped>

View File

@ -5,6 +5,9 @@
add-text="apiTestDebug.postCondition"
response=""
:height-used="600"
:show-associated-scene="props.showAssociatedScene"
:show-pre-post-request="props.showPrePostRequest"
:request-radio-text-props="props.requestRadioTextProps"
>
</condition>
</template>
@ -16,14 +19,35 @@
import { RequestConditionProcessor } from '@/enums/apiEnum';
const props = defineProps<{
showAssociatedScene?: boolean;
showPrePostRequest?: boolean;
requestRadioTextProps?: Record<string, any>;
activeTab: string;
}>();
const store = useProjectEnvStore();
const innerParams = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors =
value || [];
if (props.activeTab === 'scenarioProcessorConfig') {
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors =
value || [];
} else {
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.requestProcessorConfig.processors =
value || [];
}
},
get: () => {
if (props.activeTab === 'scenarioProcessorConfig') {
return (
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors ||
[]
);
}
return (
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.requestProcessorConfig.processors || []
);
},
get: () =>
store.currentEnvDetailInfo.config.postProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors || [],
});
</script>

View File

@ -5,6 +5,9 @@
add-text="apiTestDebug.precondition"
response=""
:height-used="600"
:show-associated-scene="props.showAssociatedScene"
:show-pre-post-request="props.showPrePostRequest"
:request-radio-text-props="props.requestRadioTextProps"
>
</condition>
</template>
@ -16,14 +19,35 @@
import { RequestConditionProcessor } from '@/enums/apiEnum';
const props = defineProps<{
showAssociatedScene?: boolean;
showPrePostRequest?: boolean;
requestRadioTextProps?: Record<string, any>;
activeTab: string;
}>();
const store = useProjectEnvStore();
const innerParams = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors =
value || [];
if (props.activeTab === 'scenarioProcessorConfig') {
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors =
value || [];
} else {
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.requestProcessorConfig.processors =
value || [];
}
},
get: () => {
if (props.activeTab === 'scenarioProcessorConfig') {
return (
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors ||
[]
);
}
return (
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.requestProcessorConfig.processors || []
);
},
get: () =>
store.currentEnvDetailInfo.config.preProcessorConfig.apiProcessorConfig.scenarioProcessorConfig.processors || [],
});
</script>

View File

@ -14,7 +14,7 @@
<a-form-item
class="mb-[16px]"
asterisk-position="end"
field="hostname"
field="url"
:label="t('project.environmental.http.hostName')"
:rules="[{ required: true, message: t('project.environmental.http.hostNameRequired') }]"
>
@ -36,7 +36,7 @@
<a-option value="https">https://</a-option>
</a-select>
<a-input
v-model="form.hostname"
v-model="form.url"
class="w-full"
:max-length="255"
:placeholder="
@ -85,8 +85,14 @@
</a-form-item> -->
<!-- 展示模块 -->
<!-- TODO 模块还没有加 -->
<!-- <a-form-item class="mb-[16px]" field="description" :label="t('project.environmental.http.selectApiModule')">
<ApiTree
<a-form-item
v-if="form.type === 'MODULE'"
class="mb-[16px]"
field="description"
:label="t('project.environmental.http.selectApiModule')"
>
<!-- TODO 先做普通树 放在下一个版本 -->
<!-- <ApiTree
v-model:focus-node-key="focusNodeKey"
:placeholder="t('project.environmental.http.selectApiModule')"
:selected-keys="selectedKeys"
@ -108,8 +114,25 @@
<template #tree-slot-extra="nodeData">
<span><MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, nodeData)" /></span>
</template>
</ApiTree>
</a-form-item> -->
</ApiTree> -->
<a-tree-select
v-model="form.moduleId"
:data="envTree"
class="w-full"
:tree-checkable="true"
:allow-search="true"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
></a-tree-select>
</a-form-item>
<!-- 路径 -->
<a-form-item
v-if="showPathInput"
@ -142,21 +165,26 @@
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import type { MsTreeFieldNames, MsTreeNodeData, MsTreeSelectedData } from '@/components/business/ms-tree/types';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import RequestHeader from '../../requestHeader/index.vue';
import { getEnvModules } from '@/api/modules/api-test/management';
// import ApiTree from './apiTree.vue';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { getGenerateId } from '@/utils';
import type { EnvModule } from '@/models/apiTest/management';
import type { ModuleTreeNode } from '@/models/common';
import { HttpForm } from '@/models/projectManagement/environmental';
const props = defineProps<{
currentId: string;
isCopy: boolean;
moduleTree: ModuleTreeNode[];
}>();
const appStore = useAppStore();
const store = useProjectEnvStore();
const emit = defineEmits<{
@ -174,7 +202,7 @@
},
];
const initForm = {
const initForm: HttpForm = {
id: '',
hostname: '',
type: 'NONE',
@ -182,7 +210,21 @@
path: '',
condition: 'CONTAINS',
description: '',
url: '',
protocol: 'http',
moduleId: [],
moduleMatchRule: {
modules: [
// {
// moduleId: '',
// containChildModule: false,
// },
],
},
pathMatchRule: {
path: '',
condition: '',
},
};
const form = ref<HttpForm>({ ...initForm });
@ -202,437 +244,59 @@
const handleAddOrUpdate = () => {
const index = store.currentEnvDetailInfo.config.httpConfig.findIndex((item) => item.id === form.value.id);
let modules: { moduleId: string; containChildModule: boolean }[] = [];
const { protocol, url, condition, path } = form.value;
if (form.value.type === 'MODULE') {
modules = form.value.moduleId.map((item) => {
return {
moduleId: item,
containChildModule: false,
};
});
}
//
if (index > -1 && !props.isCopy) {
store.currentEnvDetailInfo.config.httpConfig.splice(index + 1, 1, form.value);
const httpItem = {
...form.value,
hostname: `${protocol}:${url}`,
pathMatchRule: {
path,
condition,
},
order: store.currentEnvDetailInfo.config.httpConfig.length + 1,
moduleMatchRule: { modules },
};
store.currentEnvDetailInfo.config.httpConfig.splice(index, 1, httpItem);
//
} else if (index > -1 && props.isCopy) {
const insertItem = {
...form.value,
id: getGenerateId(),
hostname: `copy_${form.value.hostname}`,
hostname: `${protocol}:${url}`,
order: store.currentEnvDetailInfo.config.httpConfig.length + 1,
moduleMatchRule: { modules },
};
store.currentEnvDetailInfo.config.httpConfig.splice(index, 0, insertItem);
store.currentEnvDetailInfo.config.httpConfig.splice(index + 1, 0, insertItem);
//
} else {
const { protocol, hostname, condition, path } = form.value;
const httpItem = {
...form.value,
hostname: `${protocol}://${hostname}`,
hostname: `${protocol}://${url}`,
pathMatchRule: {
path,
condition,
},
id: getGenerateId(),
order: store.currentEnvDetailInfo.config.httpConfig.length + 1,
moduleMatchRule: { modules },
};
store.currentEnvDetailInfo.config.httpConfig.push(httpItem);
}
emit('close');
};
const moduleTree = ref([
{
id: 'root',
name: '未规划请求',
type: 'MODULE',
parentId: 'NONE',
children: [
{
id: '4112912223068160',
name: '随便写的',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'OPTIONS',
},
count: 0,
path: '/',
},
{
id: '1150192243335168',
name: '文件儿',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '1165379247169536',
name: '901',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'TCP',
},
count: 0,
path: '/',
},
{
id: '1165705664684032',
name: '888',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'TCP',
},
count: 0,
path: '/',
},
{
id: '2125544956010496',
name: '0129-1',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '2126988065021952',
name: '0129-2',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '2171827523477504',
name: 'fffggg',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '2297223388766208',
name: '0129-3',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '4034709458542592',
name: '测试一下百度',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'PATCH',
},
count: 0,
path: '/',
},
{
id: '1017890070175744',
name: 'TTTTTCCCCCPPPPP',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'TCP',
},
count: 0,
path: '/',
},
{
id: '864679996792832',
name: '委托',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '867463135600640',
name: '登入',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '868236229713920',
name: '买入',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '927008562290689',
name: '账号校验1',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '943707395039232',
name: 'ddd',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'TCP',
},
count: 0,
path: '/',
},
{
id: '1068845561815040',
name: 'TCP测试2',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'TCP',
},
count: 0,
path: '/',
},
{
id: '1131706704060416',
name: 'aaa',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '921184586637312',
name: '读取系统日期22',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '642853526609920',
name: 'Test',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '851760736174080',
name: 'dd',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '853891039952896',
name: 'fasdfd',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '1118186147643392',
name: 'eeee',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '1120161832599552',
name: 'eeeeqqq',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '1162149432885248',
name: 'a',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '1177147458682880',
name: '这是Curl导入的请求',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '2226957725106176',
name: 'test12',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '2601169635672064',
name: 'testvvvv',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '885467640184832',
name: 'okko',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '891635213221888',
name: 'gs',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'HTTP',
method: 'GET',
},
count: 0,
path: '/',
},
{
id: '640018849742848',
name: '0228',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
{
id: '2178166898065408',
name: '0304',
type: 'API',
parentId: 'root',
children: [],
attachInfo: {
protocol: 'SPX',
},
count: 0,
path: '/',
},
],
attachInfo: {},
count: 0,
path: '/未规划请求',
},
]);
const envTree = ref<ModuleTreeNode[]>([]);
const moreActions: ActionsItem[] = [
{
@ -654,14 +318,35 @@
(item) => item.id === props.currentId
) as HttpForm;
if (currentItem) {
const { path, condition } = currentItem.pathMatchRule;
const urlPath = currentItem.hostname.match(/\/\/(.*)/);
form.value = {
...currentItem,
moduleId: currentItem.moduleMatchRule.modules.map((item) => item.moduleId) || [],
path,
condition,
url: urlPath && urlPath?.length > 1 ? `//${urlPath[1]}` : '',
};
}
} else {
resetForm();
}
});
async function initModuleTree() {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
});
envTree.value = res.moduleTree;
} catch (error) {
console.log(error);
}
}
onBeforeMount(() => {
initModuleTree();
});
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,96 @@
<template>
<div>
<a-tabs v-model:active-key="activeTab" lazy-load class="no-content">
<a-tab-pane v-for="item of tabList" :key="item.key" :title="item.label"></a-tab-pane>
</a-tabs>
<a-divider margin="0"></a-divider>
<div v-if="activeTab === 'scenarioProcessorConfig'" class="mt-4">
<a-alert class="mb-4"> {{ t('project.environmental.sceneAlertDesc') }} </a-alert>
<PreTab
v-if="props.activeType === 'pre'"
:show-associated-scene="showAssociatedScene"
:show-pre-post-request="!showAssociatedScene"
:active-tab="activeTab"
/>
<PostTab
v-if="props.activeType === 'post'"
:show-associated-scene="showAssociatedScene"
:show-pre-post-request="!showAssociatedScene"
:active-tab="activeTab"
/>
</div>
<div v-if="activeTab === 'requestProcessorConfig'" class="mt-4">
<a-alert class="mb-4"> {{ t('project.environmental.requestAlertDesc') }} </a-alert>
<PreTab
v-if="props.activeType === 'pre'"
:show-associated-scene="showAssociatedScene"
:show-pre-post-request="!showAssociatedScene"
:request-radio-text-props="requestPropsText"
:active-tab="activeTab"
/>
<PostTab
v-if="props.activeType === 'post'"
:show-associated-scene="showAssociatedScene"
:show-pre-post-request="!showAssociatedScene"
:request-radio-text-props="requestPropsText"
:active-tab="activeTab"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import PostTab from './PostTab.vue';
import PreTab from './PreTab.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
activeType: string;
}>();
const tabList = ref<{ key: string; label: string }[]>([
{
key: 'scenarioProcessorConfig',
label: '场景',
},
{
key: 'requestProcessorConfig',
label: '请求',
},
]);
const activeTab = ref<string>('scenarioProcessorConfig');
const showAssociatedScene = computed(() => {
return activeTab.value === 'scenarioProcessorConfig';
});
const requestPropsText = computed(() => {
if (activeTab.value === 'requestProcessorConfig') {
return props.activeType === 'pre'
? {
pre: t('project.environmental.preScriptBefore'),
post: t('project.environmental.preScriptAfter'),
preTip: t('project.environmental.http.preTextPreTip'),
postTip: t('project.environmental.http.preTextPostTip'),
}
: {
pre: t('project.environmental.postScriptBefore'),
post: t('project.environmental.postScriptAfter'),
preTip: t('project.environmental.http.postTextPreTip'),
postTip: t('project.environmental.http.postTextPostTip'),
};
}
});
</script>
<style scoped lang="less">
.no-content {
:deep(.arco-tabs-content) {
padding-top: 0;
}
}
</style>

View File

@ -29,6 +29,10 @@ export default {
'project.environmental.TCP': 'TCP',
'project.environmental.pre': 'Pre',
'project.environmental.post': 'Post',
'project.environmental.sceneAlertDesc':
'Perform this operation once before scenario execution, such as obtaining a token and scenario initialization',
'project.environmental.requestAlertDesc':
'Each API step is executed once before execution, such as request content encryption',
'project.environmental.host': 'Host',
'project.environmental.assert': 'Assertion',
'project.environmental.displaySetting': 'Display Setting',
@ -87,4 +91,17 @@ export default {
'project.environmental.host.descPlaceholder': 'Please enter the description',
'project.environmental.newEnv': 'New Environment',
'project.environmental.http.hostNamePlaceholder': 'Please enter the host name',
'project.environmental.preScriptBefore': 'Pre script before',
'project.environmental.preScriptAfter': 'After pre script',
'project.environmental.postScriptBefore': 'Post script front',
'project.environmental.postScriptAfter': 'After the script',
'project.environmental.http.preTextPreTip':
'Before pre script, environment scripts executed before buy scripts in the request;',
'project.environmental.http.preTextPostTip':
'After the preset script, the script in the environment is executed after the requested preset script is executed;',
'project.environmental.http.postTextPreTip':
'The script in the environment is executed before the requested script is executed.',
'project.environmental.http.postTextPostTip':
'After rear script, environment a prerequisite for the script in the request after the script execution;',
'project.environmental.preOrPost.ignoreProtocols': 'Ignore request:',
};

View File

@ -36,6 +36,8 @@ export default {
'project.environmental.database': '数据库',
'project.environmental.pre': '前置',
'project.environmental.post': '后置',
'project.environmental.sceneAlertDesc': '场景执行前执行一次如token获取及场景初始化',
'project.environmental.requestAlertDesc': '每一个API步骤执行前均执行一次如请求内容加密',
'project.environmental.host': '域名',
'project.environmental.assert': '断言',
'project.environmental.displaySetting': '显示设置',
@ -103,4 +105,13 @@ export default {
'project.environmental.group.envGroupNameIsRequire': '环境组名称不能为空',
'project.environmental.group.envGroupPlaceholder': '请输入环境组',
'project.environmental.http.hostNamePlaceholder': '请输入域名',
'project.environmental.preScriptBefore': '前置脚本前',
'project.environmental.preScriptAfter': '前置脚本后',
'project.environmental.postScriptBefore': '后置脚本前',
'project.environmental.postScriptAfter': '后置脚本后',
'project.environmental.http.preTextPreTip': '前置脚本前,环境中脚本在请求的置脚本执行前执行;',
'project.environmental.http.preTextPostTip': '前置置脚本后,环境中脚本在请求的前置脚本执行后执行;',
'project.environmental.http.postTextPreTip': '后置脚本前,环境中脚本在请求的置脚本执行前执行;',
'project.environmental.http.postTextPostTip': '后置脚本后,环境中脚本在请求的前置脚本执行后执行;',
'project.environmental.preOrPost.ignoreProtocols': '忽略请求:',
};

View File

@ -102,13 +102,14 @@
import { enableOrOffTemplate } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useLicenseStore from '@/store/modules/setting/license';
import useTemplateStore from '@/store/modules/setting/template';
import { hasAnyPermission } from '@/utils/permission';
const { t } = useI18n();
const appStore = useAppStore();
const templateStore = useTemplateStore();
const licenseStore = useLicenseStore();
const currentOrgId = computed(() => appStore.currentOrgId);
const props = defineProps<{
@ -142,7 +143,10 @@
const confirmLoading = ref<boolean>(false);
const orgName = computed(() => {
return appStore.ordList.find((item: any) => item.id === appStore.currentOrgId)?.name;
if (licenseStore.hasLicense()) {
return appStore.ordList.find((item: any) => item.id === appStore.currentOrgId)?.name;
}
return '默认组织';
});
async function okHandler() {