feat(脑图): 支持节点自定义下拉菜单&测试规划脑图快捷配置

This commit is contained in:
baiqi 2024-09-03 15:48:31 +08:00 committed by Craftsman
parent cb72ff6b96
commit 3445189a78
9 changed files with 252 additions and 67 deletions

View File

@ -206,7 +206,7 @@
</CaseTable>
<!-- 接口用例 API -->
<ApiTable
v-if="associationType === CaseLinkEnum.API && showType === 'API'"
v-else-if="associationType === CaseLinkEnum.API && showType === 'API'"
ref="apiTableRef"
v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
@ -229,7 +229,7 @@
</ApiTable>
<!-- 接口用例 CASE -->
<ApiCaseTable
v-if="associationType === CaseLinkEnum.API && showType === 'CASE'"
v-else-if="associationType === CaseLinkEnum.API && showType === 'CASE'"
ref="caseTableRef"
v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
@ -252,7 +252,7 @@
</ApiCaseTable>
<!-- 接口场景用例 -->
<ScenarioCaseTable
v-if="associationType === CaseLinkEnum.SCENARIO"
v-else-if="associationType === CaseLinkEnum.SCENARIO"
ref="scenarioTableRef"
v-model:selectedModulesMaps="selectedModulesMaps"
v-model:selectedIds="selectedIds"
@ -637,7 +637,7 @@
selectedProtocols.value = _protocols || [];
}
function changeSyncCase(value: string | number | boolean, ev: Event) {
function changeSyncCase(value: string | number | boolean) {
if (value) {
if (props.nodeApiTestSet && props.nodeScenarioTestSet) {
apiCaseCollectionId.value = props.nodeApiTestSet?.[0]?.id ?? '';

View File

@ -16,6 +16,9 @@
:can-show-delete-menu="canShowDeleteMenu"
:disabled="!hasEditPermission"
:can-show-batch-delete="canShowBatchDelete"
:can-show-dropdown="canShowDropdown"
:dropdown-list="dropdownList"
:checked-val="checkedVal"
custom-priority
single-tag
tag-enable
@ -239,8 +242,10 @@
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import {
MinderDropdownListItem,
MinderEvent,
MinderJson,
MinderJsonNode,
@ -306,7 +311,6 @@
*/
function checkNodeCanShowMenu(node: PlanMinderNode) {
const { data } = node;
if (!hasEditPermission.value && (data?.level === 1 || data?.level === 2)) {
//
if (data?.type === PlanMinderCollectionType.FUNCTIONAL) {
@ -608,19 +612,38 @@
selectedAssociateCasesParams.value = { ...param };
const node: PlanMinderNode = window.minder.getSelectedNode();
let associateType: string = '';
if (node && node.data?.type === PlanMinderCollectionType.SCENARIO) {
let nodeDataType = node?.data?.type;
if (!extraVisible.value) {
//
nodeDataType = node?.parent?.data?.type;
}
if (nodeDataType === PlanMinderCollectionType.SCENARIO) {
associateType = PlanMinderAssociateType.SCENARIO_CASE;
} else {
associateType = param?.associateType ?? node.data.type;
associateType = param?.associateType ?? nodeDataType;
}
if (extraVisible.value) {
//
node.data.associateDTOS = [
{
...cloneDeep(param),
associateType,
},
];
} else if (node.parent?.data) {
//
node.parent.data.associateDTOS = [
{
...cloneDeep(param),
associateType,
},
];
}
node.data.associateDTOS = [
{
...cloneDeep(param),
associateType,
},
];
if (!extraVisible.value) {
// SAVE_MINDER
minderStore.dispatchEvent(MinderEventName.SAVE_MINDER);
}
caseAssociateVisible.value = false;
}
@ -645,16 +668,14 @@
*/
function associateCase() {
const node: PlanMinderNode = window.minder.getSelectedNode();
activePlanSet.value = node;
switchingConfigFormData.value = true;
configForm.value = cloneDeep(activePlanSet.value?.data);
extraVisible.value = true;
if (extraVisible.value) {
activePlanSet.value = node;
} else if (node.parent) {
activePlanSet.value = node.parent as PlanMinderNode;
}
currentSelectCase.value = (activePlanSet.value?.data.type as unknown as CaseLinkEnum) || CaseLinkEnum.FUNCTIONAL;
setCaseSelectedSet();
caseAssociateVisible.value = true;
nextTick(() => {
switchingConfigFormData.value = false;
});
}
function openCaseAssociateDrawer() {
@ -681,6 +702,19 @@
() => {
if ([MinderEventName.EXPAND, MinderEventName.COLLAPSE].includes(minderStore.event.name)) {
setCustomPriorityView(priorityTextMap);
} else if (minderStore.event.name === MinderEventName.DROPDOWN_SELECT) {
const node: PlanMinderNode = window.minder.getSelectedNode();
if (node?.data?.level === 3 && node?.data?.resource?.[0] === resourcePoolTag) {
if (node.parent?.data) {
node.parent.data.testResourcePoolId = minderStore.event.params;
minderStore.dispatchEvent(MinderEventName.SAVE_MINDER);
}
} else if (node?.data?.level === 3 && node?.data?.resource?.[0] === envTag) {
if (node.parent?.data) {
node.parent.data.environmentId = minderStore.event.params;
minderStore.dispatchEvent(MinderEventName.SAVE_MINDER);
}
}
}
}
);
@ -716,6 +750,13 @@
}
);
/**
* 是否可以显示下拉菜单
*/
const canShowDropdown = ref(false);
const dropdownList = ref<MinderDropdownListItem[]>([]);
const checkedVal = ref<string>();
/**
* 处理节点选中
* @param node 节点
@ -730,8 +771,7 @@
selectNodeExecuteMethod.value = undefined;
}
if (node.data?.level === 3 && node.data?.resource?.[0] === caseCountTag) {
window.minder.toggleSelect(node);
window.minder.selectById(node.parent?.data?.id);
canShowFloatMenu.value = false;
if (!inInsertingNode.value && hasEditPermission && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ASSOCIATION'])) {
//
associateCase();
@ -740,8 +780,21 @@
node.data?.level === 3 &&
(node.data?.resource?.[0] === resourcePoolTag || node.data?.resource?.[0] === envTag)
) {
window.minder.toggleSelect(node);
window.minder.selectById(node.parent?.data?.id);
canShowFloatMenu.value = false;
canShowDropdown.value = !node.parent?.data?.extended; //
if (node.data?.resource?.[0] === resourcePoolTag) {
dropdownList.value = resourcePoolOptions.value.map((item) => ({
label: item.label || '',
value: item.value as string,
}));
checkedVal.value = node.parent?.data?.testResourcePoolId;
} else {
dropdownList.value = environmentOptions.value.map((item) => ({
label: item.label || '',
value: item.value as string,
}));
checkedVal.value = node.parent?.data?.environmentId;
}
} else {
checkNodeCanShowMenu(node);
if (extraVisible.value) {
@ -968,7 +1021,7 @@
}
if (!configFormValidResult) return;
loading.value = true;
await editPlanMinder(makeMinderParams(extraVisible.value ? window.minder.exportJson() : fullJson));
await editPlanMinder(makeMinderParams(window.minder.exportJson()));
Message.success(t('common.saveSuccess'));
emit('save');
clearSelectedCases();

View File

@ -0,0 +1,64 @@
import useMinderStore from '@/store/modules/components/minder-editor';
import type { MinderCustomEvent, MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import { sleep } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
import type { MinderJsonNode } from '../props';
import { isNodeInMinderView } from '../script/tool/utils';
export default function useMinderTrigger(
handleSelect?: (event: MinderCustomEvent, selectedNodes: MinderJsonNode[]) => void
) {
const minderStore = useMinderStore();
const triggerVisible = ref(false);
const triggerOffset = ref([0, 0]);
watch(
() => minderStore.event.eventId,
async () => {
if (window.minder) {
let nodePosition: MinderNodePosition | undefined;
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (minderStore.event.name === MinderEventName.NODE_SELECT) {
nodePosition = minderStore.event.nodePosition;
if (handleSelect) {
handleSelect(minderStore.event, selectedNodes);
}
}
if (selectedNodes.length > 1) {
// 多选时隐藏悬浮菜单
triggerVisible.value = false;
return;
}
if ([MinderEventName.VIEW_CHANGE, MinderEventName.DRAG_FINISH].includes(minderStore.event.name)) {
// 脑图画布移动时,重新计算节点位置
await sleep(300); // 拖拽完毕后会有 300ms 的动画,等待动画结束后再计算
nodePosition = window.minder.getSelectedNode()?.getRenderBox();
}
const state = window.editor.fsm.state();
if (
nodePosition &&
isNodeInMinderView(undefined, nodePosition, Math.min(nodePosition.width / 2, 200)) &&
state !== 'input'
) {
// 判断节点在脑图可视区域内且遮挡的节点不超过节点宽度的一半(超过 200px 则按 200px 算)且当前不是编辑名称状态,则显示菜单
const nodeDomHeight = nodePosition.height || 0;
triggerOffset.value = [nodePosition.x, nodePosition.y + nodeDomHeight + 4]; // trigger显示在节点下方4px处
triggerVisible.value = true;
} else {
triggerVisible.value = false;
}
}
},
{
immediate: true,
}
);
return {
triggerVisible,
triggerOffset,
};
}

View File

@ -29,6 +29,7 @@
<slot name="extractMenu"></slot>
</template>
</nodeFloatMenu>
<nodeDropdown v-if="props.canShowDropdown" :dropdown-list="props.dropdownList" :checked-val="props.checkedVal" />
<batchMenu v-bind="props" />
</div>
</template>
@ -37,6 +38,7 @@
import { cloneDeep } from 'lodash-es';
import batchMenu from '../menu/batchMenu.vue';
import nodeDropdown from '../menu/nodeDropdown.vue';
import nodeFloatMenu from '../menu/nodeFloatMenu.vue';
import minderHeader from './header.vue';
import Navigator from './navigator.vue';
@ -50,6 +52,7 @@
import useEventListener from '../hooks/useMinderEventListener';
import {
batchMenuProps,
dropdownMenuProps,
editMenuProps,
floatMenuProps,
headerProps,
@ -72,6 +75,7 @@
...tagProps,
...priorityProps,
...batchMenuProps,
...dropdownMenuProps,
});
const emit = defineEmits<{
(e: 'save', data: MinderJson, callback: () => void): void;

View File

@ -0,0 +1,54 @@
<template>
<a-dropdown
v-model:popup-visible="triggerVisible"
class="ms-minder-node-dropdown"
:popup-translate="triggerOffset"
position="bl"
trigger="click"
@select="(val) => handleSelect(val)"
>
<span></span>
<template #content>
<a-doption
v-for="item in props.dropdownList"
:key="item.value"
v-permission="item.permission || []"
:value="item.value"
:class="props.checkedVal === item.value ? 'ms-minder-node-dropdown-item--active' : ''"
@click="item.onClick && item.onClick()"
>
<div>{{ item.label }}</div>
</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import useMinderStore from '@/store/modules/components/minder-editor';
import { MinderEventName } from '@/enums/minderEnum';
import useMinderTrigger from '../hooks/useMinderTrigger';
import { dropdownMenuProps } from '../props';
const props = defineProps(dropdownMenuProps);
const minderStore = useMinderStore();
const { triggerVisible, triggerOffset } = useMinderTrigger();
function handleSelect(val?: string | number | Record<string, any>) {
if (props.checkedVal !== val) {
minderStore.dispatchEvent(MinderEventName.DROPDOWN_SELECT, val as string);
}
}
</script>
<style lang="less">
.ms-minder-node-dropdown {
max-height: 350px;
.ms-minder-node-dropdown-item--active {
color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
}
}
</style>

View File

@ -195,15 +195,13 @@
import { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import { MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import { sleep } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
import useMinderOperation from '../hooks/useMinderOperation';
import usePriority from '../hooks/useMinderPriority';
import useMinderTrigger from '../hooks/useMinderTrigger';
import { floatMenuProps, mainEditorProps, MinderJsonNode, priorityColorMap, priorityProps, tagProps } from '../props';
import { isNodeInMinderView } from '../script/tool/utils';
const props = defineProps({
...mainEditorProps,
@ -227,45 +225,29 @@
});
const menuPopupOffset = ref<TriggerPopupTranslate>([0, 0]);
const { triggerOffset, triggerVisible } = useMinderTrigger((event, selectedNodes) => {
currentNodeTags.value = event.nodes?.[0].data?.resource || [];
if (props.replaceableTags && !props.disabled) {
tags.value = props.replaceableTags(selectedNodes);
} else {
tags.value = [];
}
});
watch(
() => minderStore.event.eventId,
async () => {
if (window.minder) {
let nodePosition: MinderNodePosition | undefined;
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (minderStore.event.name === MinderEventName.NODE_SELECT) {
nodePosition = minderStore.event.nodePosition;
currentNodeTags.value = minderStore.event.nodes?.[0].data?.resource || [];
if (props.replaceableTags && !props.disabled) {
tags.value = props.replaceableTags(selectedNodes);
} else {
tags.value = [];
}
}
if (selectedNodes.length > 1) {
//
menuVisible.value = false;
return;
}
if ([MinderEventName.VIEW_CHANGE, MinderEventName.DRAG_FINISH].includes(minderStore.event.name)) {
//
await sleep(300); // 300ms
nodePosition = window.minder.getSelectedNode()?.getRenderBox();
}
const state = window.editor.fsm.state();
if (
nodePosition &&
isNodeInMinderView(undefined, nodePosition, Math.min(nodePosition.width / 2, 200)) &&
state !== 'input'
) {
// ( 200px 200px )
const nodeDomHeight = nodePosition.height || 0;
menuPopupOffset.value = [nodePosition.x, nodePosition.y + nodeDomHeight + 4]; // 4px
menuVisible.value = true;
} else {
menuVisible.value = false;
}
}
() => triggerOffset.value,
(val) => {
menuPopupOffset.value = [val[0], val[1]];
},
{
immediate: true,
}
);
watch(
() => triggerVisible.value,
(val) => {
menuVisible.value = val;
},
{
immediate: true,

View File

@ -39,6 +39,7 @@
import {
batchMenuProps,
delProps,
dropdownMenuProps,
editMenuProps,
floatMenuProps,
headerProps,
@ -77,6 +78,7 @@
...delProps,
...viewMenuProps,
...batchMenuProps,
...dropdownMenuProps,
});
const minderStore = useMinderStore();

View File

@ -216,6 +216,31 @@ export const floatMenuProps = {
default: false,
},
};
export interface MinderDropdownListItem {
value: string;
label: string;
permission?: string[];
onClick?: () => void;
}
export const dropdownMenuProps = {
// 是否显示Dropdown
canShowDropdown: {
type: Boolean,
default: false,
},
dropdownList: {
type: Array as PropType<MinderDropdownListItem[]>,
default() {
return [];
},
},
checkedVal: {
type: String as PropType<string>,
default: undefined,
},
};
export const batchMenuProps = {
canShowMoreBatchMenu: {
type: Boolean,

View File

@ -16,6 +16,7 @@ export enum MinderEventName {
'MINDER_CHANGED' = 'MINDER_CHANGED', // 脑图更改事件
'SAVE_MINDER' = 'SAVE_MINDER', // 脑图保存事件
'DRAG_FINISH' = 'DRAG_FINISH', // 脑图节点拖拽结束事件
'DROPDOWN_SELECT' = 'DROPDOWN_SELECT', // 下拉菜单选中事件
}
export enum MinderKeyEnum {