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; @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 { &:hover {
background-color: transparent; background-color: transparent;
} }
.ms-tree-node-extra {
background-color: transparent;
}
* { * {
color: var(--color-text-4) !important; color: var(--color-text-4) !important;
} }

View File

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

View File

@ -129,27 +129,4 @@
color: rgb(var(--danger-6)); 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> </style>

View File

@ -44,8 +44,9 @@
<MsTree <MsTree
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:halfCheckedKeys="halfCheckedKeys" v-model:halfCheckedKeys="halfCheckedKeys"
:selectable="false" v-model:focus-node-key="focusNodeKey"
:data="treeData" :data="treeData"
:selectable="false"
:keyword="inputValue" :keyword="inputValue"
:empty-text="t('common.noData')" :empty-text="t('common.noData')"
:virtual-list-props="virtualListProps" :virtual-list-props="virtualListProps"
@ -57,20 +58,21 @@
:checkable="props.treeCheckable" :checkable="props.treeCheckable"
:check-strictly="props.treeCheckStrictly" :check-strictly="props.treeCheckStrictly"
v-bind="$attrs" v-bind="$attrs"
@check="checkNode" @check="handleCheck"
> >
<template #title="nodeData"> <template #title="nodeData">
<div <div
class="one-line-text w-full cursor-pointer text-[var(--color-text-1)]" 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 }} {{ nodeData.name }}
</div> </div>
</template> </template>
<template #extra="nodeData"> <template #extra="nodeData">
<MsButton <MsButton
v-if="nodeData.children && nodeData.children.length" v-if="nodeData.children && nodeData.children.length && !nodeData.disabled"
@click="selectParent(nodeData, !!checkedKeys.includes(nodeData.id))" class="!mr-[8px]"
@click="handleSelectCurrent(nodeData)"
> >
{{ {{
checkedKeys.includes(nodeData.id) checkedKeys.includes(nodeData.id)
@ -78,6 +80,15 @@
: t('ms.case.associate.selectCurrent') : t('ms.case.associate.selectCurrent')
}} }}
</MsButton> </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> </template>
</MsTree> </MsTree>
</template> </template>
@ -92,6 +103,7 @@
import useTreeSelection from '@/components/business/ms-associate-case/useTreeSelection'; import useTreeSelection from '@/components/business/ms-associate-case/useTreeSelection';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types';
import MoreMenuDropdown from './moreMenuDropdown.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useSelect from '@/hooks/useSelect'; import useSelect from '@/hooks/useSelect';
@ -101,12 +113,12 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
data: TreeNodeData[];
fieldNames?: TreeFieldNames | MsTreeFieldNames; fieldNames?: TreeFieldNames | MsTreeFieldNames;
multiple?: boolean; multiple?: boolean;
shouldCalculateMaxTag?: boolean; shouldCalculateMaxTag?: boolean;
treeCheckStrictly?: boolean; treeCheckStrictly?: boolean;
treeCheckable?: boolean; treeCheckable?: boolean;
showContainChildModule?: boolean;
}>(), }>(),
{ {
shouldCalculateMaxTag: true, shouldCalculateMaxTag: true,
@ -126,6 +138,66 @@
const { selectedModulesMaps, checkedKeys, halfCheckedKeys, selectParent, checkNode, clearSelector } = const { selectedModulesMaps, checkedKeys, halfCheckedKeys, selectParent, checkNode, clearSelector } =
useTreeSelection(selectedModuleProps.value); 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); const skipSelectValueWatch = ref(false);
watch( watch(
() => selectValue.value, () => selectValue.value,
@ -170,7 +242,7 @@
} }
); );
watch( watch(
() => props.data, () => treeData.value,
() => { () => {
if (props.shouldCalculateMaxTag !== false && props.multiple) { if (props.shouldCalculateMaxTag !== false && props.multiple) {
calculateMaxTag(); calculateMaxTag();
@ -182,11 +254,11 @@
return () => { return () => {
let treeSelectTooltip = ''; let treeSelectTooltip = '';
const values = Array.isArray(checkedKeys.value) ? checkedKeys.value : [checkedKeys.value]; const values = Array.isArray(checkedKeys.value) ? checkedKeys.value : [checkedKeys.value];
if (props.data) { if (treeData.value) {
treeSelectTooltip = values treeSelectTooltip = values
?.map((valueItem: string | number) => { ?.map((valueItem: string | number) => {
const optItem = findNodeByKey<MsTreeNodeData>( const optItem = findNodeByKey<MsTreeNodeData>(
props.data as MsTreeNodeData[], treeData.value as MsTreeNodeData[],
valueItem, valueItem,
props?.fieldNames?.key props?.fieldNames?.key
); );
@ -272,6 +344,14 @@
buffer: 15, // 10 padding buffer: 15, // 10 padding
}; };
}); });
const focusNodeKey = ref<string | number>('');
function setFocusKey(node: MsTreeNodeData) {
focusNodeKey.value = node.id || '';
}
function resetFocusNodeKey() {
focusNodeKey.value = '';
}
</script> </script>
<style lang="less"> <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; moduleId: string;
containChildModule: boolean; // 是否包含新增子模块 containChildModule: boolean; // 是否包含新增子模块
disabled: boolean; disabled?: boolean;
} }
// 定义-获取环境的模块树参数 // 定义-获取环境的模块树参数

View File

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

View File

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

View File

@ -68,6 +68,9 @@ export default {
'project.environmental.http.uiModuleSelect': 'Select UI Test Module', 'project.environmental.http.uiModuleSelect': 'Select UI Test Module',
'project.environmental.http.pathRequired': 'Path is required', 'project.environmental.http.pathRequired': 'Path is required',
'project.environmental.http.pathPlaceholder': 'Please enter the path', '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.addDatabase': 'Add Database',
'project.environmental.database.updateDatabase': 'Update Database {name}', 'project.environmental.database.updateDatabase': 'Update Database {name}',
'project.environmental.database.name': 'Database Name', 'project.environmental.database.name': 'Database Name',

View File

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