feat(项目管理): 环境管理-添加http-模块增加包含新增子模块的功能

This commit is contained in:
teukkk 2024-11-15 18:01:35 +08:00 committed by Craftsman
parent cbc8cd8a7a
commit 1032b30df4
11 changed files with 235 additions and 82 deletions

View File

@ -190,3 +190,28 @@ body {
@apply mt-0;
}
}
/** 更多按钮 **/
.ms-more-action-trigger-content {
@apply flex items-center;
.more-icon-btn {
padding: 2px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9)) !important;
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
}
}
.ms-more-action-trigger-content--focus {
.more-icon-btn {
@apply !visible;
background-color: rgb(var(--primary-9));
.arco-icon {
color: rgb(var(--primary-5));
}
}
}

View File

@ -629,6 +629,9 @@
&:hover {
background-color: transparent;
}
.ms-tree-node-extra {
background-color: transparent;
}
* {
color: var(--color-text-4) !important;
}

View File

@ -11,6 +11,7 @@ export type MsTreeNodeData = {
hideMoreAction?: boolean; // 隐藏更多操作
parentId?: string;
expanded?: boolean; // 是否展开
containChildModule?: boolean; // 包含子模块
[key: string]: any;
} & TreeNodeData;

View File

@ -129,27 +129,4 @@
color: rgb(var(--danger-6));
}
}
.ms-more-action-trigger-content {
@apply flex items-center;
.more-icon-btn {
padding: 2px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9)) !important;
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
}
}
.ms-more-action-trigger-content--focus {
.more-icon-btn {
@apply !visible;
background-color: rgb(var(--primary-9));
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -44,8 +44,9 @@
<MsTree
v-model:checked-keys="checkedKeys"
v-model:halfCheckedKeys="halfCheckedKeys"
:selectable="false"
v-model:focus-node-key="focusNodeKey"
:data="treeData"
:selectable="false"
:keyword="inputValue"
:empty-text="t('common.noData')"
:virtual-list-props="virtualListProps"
@ -57,20 +58,21 @@
:checkable="props.treeCheckable"
:check-strictly="props.treeCheckStrictly"
v-bind="$attrs"
@check="checkNode"
@check="handleCheck"
>
<template #title="nodeData">
<div
class="one-line-text w-full cursor-pointer text-[var(--color-text-1)]"
@click="checkNode(checkedKeys, { checked: !checkedKeys.includes(nodeData.id), node: nodeData })"
@click="handleCheck(checkedKeys, { checked: !checkedKeys.includes(nodeData.id), node: nodeData })"
>
{{ nodeData.name }}
</div>
</template>
<template #extra="nodeData">
<MsButton
v-if="nodeData.children && nodeData.children.length"
@click="selectParent(nodeData, !!checkedKeys.includes(nodeData.id))"
v-if="nodeData.children && nodeData.children.length && !nodeData.disabled"
class="!mr-[8px]"
@click="handleSelectCurrent(nodeData)"
>
{{
checkedKeys.includes(nodeData.id)
@ -78,6 +80,15 @@
: t('ms.case.associate.selectCurrent')
}}
</MsButton>
<MoreMenuDropdown
v-if="props.showContainChildModule && !nodeData.disabled && nodeData.children && nodeData.children.length"
v-model:contain-child-module="nodeData.containChildModule"
@handle-contain-child-module="
(containChildModule) => handleContainChildModule(nodeData, containChildModule)
"
@close="resetFocusNodeKey"
@open="setFocusKey(nodeData)"
/>
</template>
</MsTree>
</template>
@ -92,6 +103,7 @@
import useTreeSelection from '@/components/business/ms-associate-case/useTreeSelection';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types';
import MoreMenuDropdown from './moreMenuDropdown.vue';
import { useI18n } from '@/hooks/useI18n';
import useSelect from '@/hooks/useSelect';
@ -101,12 +113,12 @@
const props = withDefaults(
defineProps<{
data: TreeNodeData[];
fieldNames?: TreeFieldNames | MsTreeFieldNames;
multiple?: boolean;
shouldCalculateMaxTag?: boolean;
treeCheckStrictly?: boolean;
treeCheckable?: boolean;
showContainChildModule?: boolean;
}>(),
{
shouldCalculateMaxTag: true,
@ -126,6 +138,66 @@
const { selectedModulesMaps, checkedKeys, halfCheckedKeys, selectParent, checkNode, clearSelector } =
useTreeSelection(selectedModuleProps.value);
/**
* 设置子节点的属性值
* @param trees 属性数组
* @param targetKey 需要匹配的属性值
*/
function updateChildNodesState(node: MsTreeNodeData, targetKey: keyof MsTreeNodeData, state: boolean) {
if (node.children) {
node.children.forEach((child: MsTreeNodeData) => {
child[targetKey] = state;
updateChildNodesState(child, targetKey, state);
});
}
}
function handleCheck(_checkedKeys: Array<string | number>, checkedNodes: MsTreeNodeData) {
if (props.showContainChildModule) {
const realNode = findNodeByKey<MsTreeNodeData>(treeData.value, checkedNodes.node.id, 'id');
if (!realNode) return;
if (checkedNodes.checked) {
//
if (realNode.containChildModule) {
updateChildNodesState(realNode, 'containChildModule', true);
updateChildNodesState(realNode, 'disabled', true);
}
} else {
//
realNode.containChildModule = false;
updateChildNodesState(realNode, 'containChildModule', false);
updateChildNodesState(realNode, 'disabled', false);
}
}
checkNode(_checkedKeys, checkedNodes);
}
function handleSelectCurrent(nodeData: MsTreeNodeData) {
if (props.showContainChildModule && checkedKeys.value.includes(nodeData.id)) {
//
const realNode = findNodeByKey<MsTreeNodeData>(treeData.value, nodeData.id, 'id');
if (!realNode) return;
realNode.containChildModule = false;
realNode.children?.forEach((child) => {
child.disabled = false;
});
}
selectParent(nodeData, !!checkedKeys.value.includes(nodeData.id));
}
function handleContainChildModule(nodeData: MsTreeNodeData, containChildModule: boolean) {
const realNode = findNodeByKey<MsTreeNodeData>(treeData.value, nodeData.id, 'id');
if (!realNode) return;
realNode.containChildModule = containChildModule;
if (containChildModule) {
handleCheck(checkedKeys.value, { checked: true, node: realNode });
} else {
realNode.children?.forEach((child) => {
child.disabled = false;
});
}
}
const skipSelectValueWatch = ref(false);
watch(
() => selectValue.value,
@ -170,7 +242,7 @@
}
);
watch(
() => props.data,
() => treeData.value,
() => {
if (props.shouldCalculateMaxTag !== false && props.multiple) {
calculateMaxTag();
@ -182,11 +254,11 @@
return () => {
let treeSelectTooltip = '';
const values = Array.isArray(checkedKeys.value) ? checkedKeys.value : [checkedKeys.value];
if (props.data) {
if (treeData.value) {
treeSelectTooltip = values
?.map((valueItem: string | number) => {
const optItem = findNodeByKey<MsTreeNodeData>(
props.data as MsTreeNodeData[],
treeData.value as MsTreeNodeData[],
valueItem,
props?.fieldNames?.key
);
@ -272,6 +344,14 @@
buffer: 15, // 10 padding
};
});
const focusNodeKey = ref<string | number>('');
function setFocusKey(node: MsTreeNodeData) {
focusNodeKey.value = node.id || '';
}
function resetFocusNodeKey() {
focusNodeKey.value = '';
}
</script>
<style lang="less">

View File

@ -0,0 +1,62 @@
<template>
<a-dropdown
v-model:popup-visible="visible"
class="contain-child-dropdown"
position="br"
trigger="click"
:hide-on-select="false"
>
<div :class="['ms-more-action-trigger-content', visible ? 'ms-more-action-trigger-content--focus' : '']">
<MsButton type="text" size="mini" class="more-icon-btn" @click="visible = !visible">
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
</MsButton>
</div>
<template #content>
<a-doption>
<a-checkbox
v-model="containChildModule"
@change="
(containChildModule: boolean | (string | number | boolean)[]) =>
emit('handleContainChildModule', containChildModule as boolean)
"
>
{{ t('project.environmental.http.containChildModule') }}
</a-checkbox>
<a-tooltip :content="t('project.environmental.http.containChildModuleTip')" position="br">
<MsIcon
class="text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
type="icon-icon-maybe_outlined"
/>
</a-tooltip>
</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import { useI18n } from '@/hooks/useI18n';
const emit = defineEmits<{
(e: 'close'): void;
(e: 'open'): void;
(e: 'handleContainChildModule', containChildModule: boolean): void;
}>();
const { t } = useI18n();
const visible = ref(false);
const containChildModule = defineModel<boolean>('containChildModule', { required: false, default: false });
watch(
() => visible.value,
(val) => {
if (val) {
emit('open');
} else {
emit('close');
}
}
);
</script>

View File

@ -114,7 +114,7 @@ export interface SelectedModule {
// 选中的模块
moduleId: string;
containChildModule: boolean; // 是否包含新增子模块
disabled: boolean;
disabled?: boolean;
}
// 定义-获取环境的模块树参数

View File

@ -1,4 +1,5 @@
import { EnableKeyValueParam, ExecuteConditionProcessor } from '@/models/apiTest/common';
import type { SelectedModule } from '@/models/apiTest/management';
import { RequestAuthType } from '@/enums/apiEnum';
export interface EnvListItem {
@ -154,10 +155,7 @@ export interface HttpForm {
condition: string;
moduleId: string[];
moduleMatchRule: {
modules: {
moduleId: string;
containChildModule: boolean;
}[];
modules: SelectedModule[];
};
url: string;
pathMatchRule: {

View File

@ -107,31 +107,17 @@
<span><MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, nodeData)" /></span>
</template>
</ApiTree> -->
<a-tree-select
v-model="form.moduleId"
:data="envTree"
class="w-full"
<MsTreeSelect
v-model:model-value="form.moduleId"
v-model:data="envTree"
allow-clear
:multiple="true"
tree-check-strictly
:tree-checkable="true"
allow-search
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:filter-tree-node="filterTreeNode"
tree-checked-strategy="child"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[300px]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
show-contain-child-module
:placeholder="t('common.pleaseSelect')"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
/>
</a-form-item>
<!-- 路径 -->
<a-form-item
@ -228,13 +214,16 @@
import { Message, ValidatedError } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTreeSelect from '@/components/pure/ms-tree-select/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getEnvModules } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { filterTreeNode, getGenerateId } from '@/utils';
import { findNodeByKey, getGenerateId, mapTree } from '@/utils';
import type { SelectedModule } from '@/models/apiTest/management';
import type { ModuleTreeNode } from '@/models/common';
import { HttpForm } from '@/models/projectManagement/environmental';
import { RequestAuthType } from '@/enums/apiEnum';
@ -244,7 +233,6 @@
const props = defineProps<{
currentId: string;
isCopy: boolean;
moduleTree: ModuleTreeNode[];
}>();
const appStore = useAppStore();
const store = useProjectEnvStore();
@ -307,6 +295,9 @@
const visible = defineModel('visible', { required: true, type: Boolean, default: false });
const envTree = ref<MsTreeNodeData[]>([]);
const moduleTree = ref<MsTreeNodeData[]>([]);
const { t } = useI18n();
function resetForm() {
@ -323,7 +314,7 @@
modules = form.value.moduleId.map((item) => {
return {
moduleId: item,
containChildModule: false,
containChildModule: findNodeByKey<MsTreeNodeData>(envTree.value, item, 'id')?.containChildModule ?? false,
};
});
}
@ -378,11 +369,26 @@
});
};
const envTree = ref<ModuleTreeNode[]>([]);
const title = ref<string>('');
function initHttpDetail() {
async function initModuleTree(selectedModules?: SelectedModule[]) {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
selectedModules,
});
moduleTree.value = res.moduleTree;
store.currentEnvDetailInfo.config.httpConfig.forEach((item) => {
if (item.id === props.currentId) {
item.moduleMatchRule.modules = res.selectedModules;
}
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function initHttpDetail() {
title.value = props.currentId ? t('project.environmental.http.edit') : t('project.environmental.http.add');
if (props.isCopy) {
title.value = t('project.environmental.http.copy');
@ -391,6 +397,15 @@
const currentItem = store.currentEnvDetailInfo.config.httpConfig.find(
(item) => item.id === props.currentId
) as HttpForm;
await initModuleTree(currentItem.moduleMatchRule.modules);
envTree.value = mapTree<ModuleTreeNode>(moduleTree.value, (node) => {
return {
...node,
containChildModule:
currentItem.moduleMatchRule.modules.find((item) => item.moduleId === node.id)?.containChildModule ?? false,
disabled: !!node.parent?.containChildModule,
};
});
if (currentItem) {
const { path, condition } = currentItem.pathMatchRule;
const urlPath = currentItem.url.match(/\/\/(.*)/);
@ -403,21 +418,12 @@
};
}
} else {
await initModuleTree();
envTree.value = moduleTree.value;
resetForm();
}
}
async function initModuleTree() {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
});
envTree.value = res.moduleTree;
} catch (error) {
console.log(error);
}
}
watch(
() => visible.value,
(val) => {
@ -431,10 +437,6 @@
visible.value = false;
resetForm();
};
onBeforeMount(() => {
initModuleTree();
});
</script>
<style lang="less" scoped>

View File

@ -68,6 +68,9 @@ export default {
'project.environmental.http.uiModuleSelect': 'Select UI Test Module',
'project.environmental.http.pathRequired': 'Path is required',
'project.environmental.http.pathPlaceholder': 'Please enter the path',
'project.environmental.http.containChildModule': 'Include newly added submodules',
'project.environmental.http.containChildModuleTip':
'Automatically include sub modules added after the selected module',
'project.environmental.database.addDatabase': 'Add Database',
'project.environmental.database.updateDatabase': 'Update Database {name}',
'project.environmental.database.name': 'Database Name',

View File

@ -71,6 +71,8 @@ export default {
'project.environmental.http.uiModuleSelect': '选择UI测试模块',
'project.environmental.http.pathRequired': '路径必填',
'project.environmental.http.pathPlaceholder': '请输入路径',
'project.environmental.http.containChildModule': '包含新增子模块',
'project.environmental.http.containChildModuleTip': '自动包含所选模块后续添加的子模块',
'project.environmental.database.title': '数据库',
'project.environmental.database.addDatabase': '添加数据源',
'project.environmental.database.updateDatabase': '更新数据源{name}',