feat(脑图): 脑图组件导航工具栏

This commit is contained in:
baiqi 2024-06-13 16:04:25 +08:00 committed by 刘瑞斌
parent 430b0add87
commit 52b046cd71
13 changed files with 301 additions and 389 deletions

View File

@ -474,7 +474,7 @@
node.expand();
node.renderTree();
window.minder.layout();
window.minder.execCommand('camera', node, 600);
window.minder.execCommand('camera', node, 100);
if (node.data) {
node.data.isLoaded = true;
}

View File

@ -677,6 +677,12 @@
});
window.minder.importJson(importJson.value);
window.minder.execCommand('template', Object.keys(window.kityminder.Minder.getTemplateList())[3]);
setTimeout(() => {
//
const position = window.minder.getViewDragger().getMovement();
position.x -= position.x - 40;
window.minder.getViewDragger().moveTo(position);
}, 200);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -0,0 +1,22 @@
export default {
'ms.minders.failStop': 'Failure stop',
'ms.minders.failRetry': 'Retry on failure',
'ms.minders.stepRetry': 'Step Retry',
'ms.minders.scenarioRetry': 'Scenario Retry',
'ms.minders.retry': 'Retry',
'ms.minders.retryTimes': '(times)',
'ms.minders.retrySpace': 'Each interval',
'ms.minders.retrySpaces': '(ms)',
'ms.minders.extend': 'Inheriting parent configuration',
'ms.minders.testSet': 'Test Set',
'ms.minders.executeMethod': 'Operation mode',
'ms.minders.serial': 'S',
'ms.minders.parallel': 'P',
'ms.minders.defaultTestSet': 'Default test set',
'ms.minders.caseCount': 'Case Count',
'ms.minders.resourcePool': 'Resource pool',
'ms.minders.defaultResourcePool': 'Default Resource Pool',
'ms.minders.env': 'Environment',
'ms.minders.item': '{count} Strip',
'ms.minders.unsavedTip': 'Please save your changed configuration first!',
};

View File

@ -76,7 +76,7 @@ export default {
amplification: 'Amplification',
narrow: 'Narrow',
drag: 'Drag',
locating_root: 'Locating root node',
locating_root: 'Back to the central node',
navigator: 'Navigator',
},
history: {
@ -110,6 +110,8 @@ export default {
enterNode: 'Enter the current node',
},
loading: 'Mind map loading...',
unSavedEnterNodeTip: 'There are currently unsaved changes, please save before entering the node',
shortcut: 'Shortcut',
shortcutTitle: 'Shortcut information',
expand: 'Expand/Collapse',
},
};

View File

@ -70,7 +70,7 @@ export default {
amplification: '放大',
narrow: '缩小',
drag: '拖拽',
locating_root: '定位根节点',
locating_root: '回到中心节点',
navigator: '导航器',
},
history: {
@ -104,6 +104,8 @@ export default {
enterNode: '进入当前节点',
},
loading: '脑图加载中...',
unSavedEnterNodeTip: '当前有未保存的改动,请先保存后再进入节点',
shortcut: '快捷键',
shortcutTitle: '快捷键说明',
expand: '展开/收起',
},
};

View File

@ -1,53 +1,141 @@
<template>
<div class="navigator">
<div class="nav-bar">
<div
class="nav-btn zoom-in"
:title="t('minder.main.navigator.amplification')"
:class="{ active: zoomRadioIn }"
@click="zoomIn"
<div class="ms-minder-navigator">
<div class="ms-minder-navigator-bar">
<a-slider
v-model:model-value="zoomPercent"
:min="0"
:max="150"
:step="1"
:style="{ width: '100px' }"
:format-tooltip="formatter"
:marks="{
50: '',
}"
class="ml-[8px]"
@change="changeZoom"
/>
<a-tooltip :content="t('minder.main.navigator.drag')">
<MsButton
type="icon"
class="ms-minder-navigator-bar-icon-button"
:class="enableHand ? 'ms-minder-navigator-bar-icon-button--focus' : ''"
@click="hand"
>
<icon-drag-arrow class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-tooltip :content="t('minder.main.navigator.navigator')">
<MsButton
type="icon"
class="ms-minder-navigator-bar-icon-button"
:class="isNavOpen ? 'ms-minder-navigator-bar-icon-button--focus' : ''"
@click="toggleNavOpen"
>
<MsIcon type="icon-icon_frame_select" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-tooltip :content="t('minder.main.navigator.locating_root')">
<MsButton type="icon" class="ms-minder-navigator-bar-icon-button" @click="locateToOrigin">
<MsIcon type="icon-icon_aiming" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-trigger
:popup-translate="[5, -105]"
position="right"
class="ms-minder-shortcut-trigger"
@popup-visible-change="(val) => (shortcutTriggerVisible = val)"
>
<div class="icon" />
</div>
<div ref="zoomPan" class="zoom-pan">
<div class="origin" :style="{ transform: 'translate(0, ' + getHeight(100) + 'px)' }" @click="RestoreSize" />
<div
class="indicator"
:style="{
transform: 'translate(0, ' + getHeight(zoom) + 'px)',
transition: 'transform 200ms',
}"
/>
</div>
<div
class="nav-btn zoom-out"
:title="t('minder.main.navigator.narrow')"
:class="{ active: zoomRadioOut }"
@click="zoomOut"
>
<div class="icon" />
</div>
<div class="nav-btn hand" :title="t('minder.main.navigator.drag')" :class="{ active: enableHand }" @click="hand">
<div class="icon" />
</div>
<div class="nav-btn camera" :title="t('minder.main.navigator.locating_root')" @click="locateToOrigin">
<div class="icon" />
</div>
<div
class="nav-btn nav-trigger"
:class="{ active: isNavOpen }"
:title="t('minder.main.navigator.navigator')"
@click="toggleNavOpen"
>
<div class="icon" />
</div>
<MsButton
type="icon"
class="ms-minder-navigator-bar-icon-button"
:class="shortcutTriggerVisible ? 'ms-minder-navigator-bar-icon-button--focus' : ''"
@click="locateToOrigin"
>
<MsIcon type="icon-icon_keyboard" class="text-[var(--color-text-4)]" />
</MsButton>
<template #content>
<div class="mb-[4px] text-[14px] font-medium">{{ t('minder.shortcutTitle') }}</div>
<div class="ms-minder-shortcut-trigger-list">
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.expand') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">/</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.copy') }}</div>
<div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon">
<icon-command :size="14" />
</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">C</div>
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.insetBrother') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">
<MsIcon type="icon-icon_carriage_return2" />
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.paste') }}</div>
<div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon">
<icon-command :size="14" />
</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">V</div>
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.insetSon') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto">
Shift+Tab
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.main.history.undo') }}</div>
<div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon">
<icon-command :size="14" />
</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">Z</div>
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.enterNode') }}</div>
<div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon">
<icon-command :size="14" />
</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">
<MsIcon type="icon-icon_carriage_return2" />
</div>
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.main.history.redo') }}</div>
<div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon">
<icon-command :size="14" />
</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">Y</div>
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.delete') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">
<MsIcon type="icon-icon_carriage_return1" />
</div>
</div>
</div>
</template>
</a-trigger>
</div>
<div v-show="isNavOpen" ref="navPreviewer" class="nav-previewer" />
<div v-show="isNavOpen" ref="navPreviewer" class="ms-minder-navigator-previewer" />
</div>
</template>
<script lang="ts" name="navigator" setup>
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
<script lang="ts" setup>
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
@ -56,10 +144,8 @@
const { t } = useI18n();
const zoomPan: Ref<HTMLDivElement | null> = ref(null);
const navPreviewer: Ref<HTMLDivElement | null> = ref(null);
const zoom = ref(100);
const isNavOpen = ref(true);
const previewNavigator: Ref<HTMLDivElement | null> = ref(null);
const contentView = ref('');
@ -71,69 +157,41 @@
let paper = reactive<any>({});
let minder = reactive<any>({});
const config = reactive({
//
ctrlPanelMin: 250,
//
ctrlPanelWidth: parseInt(window.localStorage.getItem('__dev_minder_ctrlPanelWidth') || '', 10) || 250,
// 线
dividerWidth: 3,
//
defaultLang: 'zh-cn',
//
zoom: [10, 20, 30, 50, 80, 100, 120, 150, 200],
});
const enableHand = ref(minder && minder.queryCommandState && minder.queryCommandState('hand') === 1);
//
function getNavOpenState() {
return getLocalStorage('navigator-hidden');
}
function zoomIn() {
const zoomPercent = ref(50); // 100% 50% 50
/**
* 缩放
* @param value 缩放值
*/
function changeZoom(value: number | [number, number]) {
if (minder && minder.execCommand) {
minder.execCommand('zoomIn');
minder.execCommand('zoom', (value as number) + 50);
}
}
function RestoreSize() {
if (minder && minder.execCommand) {
minder.execCommand('zoom', 100);
}
}
function zoomOut() {
if (minder && minder.execCommand) {
minder.execCommand('zoomOut');
}
function formatter(value: number) {
// 0 50%+ 50
return `${value + 50}%`;
}
const enableHand = ref(false);
/**
* 开启拖拽模式
*/
function hand() {
if (minder && minder.execCommand) {
minder.execCommand('hand');
enableHand.value = minder.queryCommandState && minder.queryCommandState('hand') === 1;
}
}
function getZoomRadio(value: number) {
try {
if (!minder) return 2;
} catch (e) {
// windowminderundefined
return 2;
}
const zoomStack = minder && minder.getOption && minder.getOption('zoom');
if (!zoomStack) {
return 2;
}
const minValue = zoomStack[0];
const maxValue = zoomStack[zoomStack.length - 1];
const valueRange = maxValue - minValue;
return 1 - (value - minValue) / valueRange;
}
const zoomRadioIn = computed(() => getZoomRadio(zoom.value) === 0);
const zoomRadioOut = computed(() => getZoomRadio(zoom.value) === 1);
function getHeight(value: number) {
const totalHeight = Number(zoomPan.value?.style.height);
return getZoomRadio(value) * totalHeight;
}
/**
* 回到根节点
*/
function locateToOrigin() {
if (minder && minder.execCommand && minder.getRoot) {
minder.execCommand('camera', minder.getRoot(), 600);
@ -166,6 +224,9 @@
}
}
/**
* 更新导航器视图
*/
function updateContentView() {
if (!minder.getRenderContainer || !paper.setViewBox || !minder.getRoot) return;
const view = minder.getRenderContainer().getBoundaryBox();
@ -264,6 +325,8 @@
});
}
const shortcutTriggerVisible = ref(false);
onMounted(() => {
nextTick(() => {
minder = window.minder;
@ -285,14 +348,9 @@
visibleView = new kity.Box();
pathHandler = getPathHandler(minder.getTheme ? minder.getTheme() : '');
if (minder.setDefaultOptions) {
minder.setDefaultOptions({
zoom: config.zoom,
});
}
minder.on('zoom', (e: any) => {
zoom.value = e.zoom;
zoomPercent.value = e.zoom - 50;
});
if (isNavOpen.value) {
bind();
@ -311,8 +369,99 @@
});
</script>
<style lang="less" scoped>
.nav-btn .icon {
background-image: url('@/assets/images/minder/icons.png');
<style lang="less">
.ms-minder-shortcut-trigger {
.arco-trigger-content {
@apply w-auto bg-white;
padding: 16px;
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
.ms-minder-shortcut-trigger-list {
@apply grid grid-cols-2;
gap: 8px 12px;
.ms-minder-shortcut-trigger-listitem {
@apply flex items-center justify-between;
padding: 4px 8px;
width: 190px;
font-size: 12px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
line-height: 16px;
.ms-minder-shortcut-trigger-listitem-icon {
@apply flex items-center justify-center font-medium;
width: 22px;
height: 22px;
font-size: 12px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
color: var(--color-text-4);
line-height: 16px;
}
.ms-minder-shortcut-trigger-listitem-icon-auto {
padding: 2px 4px;
width: auto;
}
}
}
}
}
</style>
<style lang="less" scoped>
.ms-minder-navigator {
@apply absolute;
bottom: 6px;
left: 6px;
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
.ms-minder-navigator-bar {
@apply flex w-auto items-center bg-white;
padding: 4px 8px;
gap: 8px;
border-radius: var(--border-radius-small);
.ms-minder-navigator-bar-icon-button {
@apply !mr-0;
&:hover {
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-4)) !important;
}
}
}
.ms-minder-navigator-bar-icon-button--focus {
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
:deep(.arco-slider-with-marks) {
@apply mb-0 p-0;
}
}
.ms-minder-navigator-previewer {
@apply absolute cursor-crosshair bg-white;
bottom: 36px;
left: 45px;
z-index: 9;
padding: 8px;
width: 240px;
height: 160px;
border-radius: var(--border-radius-small);
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
transition: -webkit-transform 0.7s 0.1s ease;
transition: transform 0.7s 0.1s ease;
.grab {
@apply cursor-grabbing;
}
:deep(svg) {
background-color: white !important;
}
}
}
</style>

View File

@ -73,6 +73,8 @@
...viewMenuProps,
});
const minderStore = useMinderStore();
const loading = defineModel<boolean>('loading', {
default: false,
});
@ -90,7 +92,15 @@
}),
});
const minderStore = useMinderStore();
watch(
() => extraVisible.value,
(val) => {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (val && node) {
window.minder.execCommand('camera', node, 100);
}
}
);
onMounted(async () => {
window.minderProps = props;

View File

@ -1,5 +1,4 @@
import useMinderStore from '@/store/modules/components/minder-editor';
import type { MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import { MinderEventName } from '@/enums/minderEnum';

View File

@ -1,6 +1,4 @@
@import url('@7polo/kityminder-core/dist/kityminder.core.css');
@import url('navigator.less');
@import url('hotbox.less');
.km-editor {
@apply overflow-hidden;

View File

@ -1,21 +0,0 @@
@import 'dropdown-list.less';
.mind-tab-panel {
@apply w-full;
.menu-container {
@apply flex;
height: 60px;
.menu-group {
@apply flex;
padding: 4px 8px;
gap: 4px;
&:not(:last-child) {
border-right: 1px dashed #eeeeee;
}
.menu-item {
@apply flex flex-col items-center justify-center;
}
}
}
}

View File

@ -1,146 +0,0 @@
.hotbox {
@apply absolute left-0 top-0 overflow-visible;
font-family: Arial, 'PingFang SC', 'Helvetica Neue', 'Hiragino Sans GB', 'Microsoft YaHei', 'WenQuanYi Micro Hei',
sans-serif;
.state {
@apply absolute hidden overflow-visible;
.center,
.ring {
.button {
@apply absolute rounded-full;
margin-top: -35px;
margin-left: -35px;
width: 70px;
height: 70px;
box-shadow: 0 0 30px rgb(0 0 0 / 30%);
}
.label,
.key {
@apply block text-center;
line-height: 1.4em;
}
.label {
@apply font-normal text-black;
margin-top: 17px;
font-size: 16px;
line-height: 24px;
}
.key {
font-size: 12px;
color: #999999;
line-height: 16px;
}
}
.ring-shape {
@apply absolute box-content rounded-full;
top: -25px;
left: -25px;
border: 25px solid rgb(0 0 0 / 30%);
}
.top,
.bottom {
@apply absolute whitespace-nowrap;
.button {
@apply relative inline-block;
margin: 0 10px;
padding: 8px 15px;
border-radius: 15px;
box-shadow: 0 0 30px rgb(0 0 0 / 30%);
.label {
@apply align-middle text-black;
font-size: 14px;
line-height: 14px;
}
.key {
@apply align-middle;
margin-left: 3px;
font-size: 12px;
color: #999999;
line-height: 16px;
&::before {
content: '(';
}
&::after {
content: ')';
}
}
}
}
.button {
@apply cursor-default overflow-hidden;
background-color: #f9f9f9;
.key,
.label {
opacity: 0.3;
}
}
.button.enabled {
@apply bg-white;
.key,
.label {
@apply opacity-100;
}
&:hover {
background: lighten(rgb(228 93 92), 5%);
.label {
@apply text-white;
}
.key {
color: lighten(rgb(228 93 92), 30%);
}
}
&.selected {
background-color: rgb(228 93 92);
animation: selected 0.1s ease;
.label {
@apply text-white;
}
.key {
color: lighten(rgb(228 93 92), 30%);
}
}
&.pressed,
&:active {
@apply bg-[#ff974d];
.label {
@apply text-white;
}
.key {
color: lighten(#ff974d, 30%);
}
}
}
}
.state.active {
@apply block;
}
}
@keyframes selected {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.hotbox-key-receiver {
@apply absolute m-0 outline-none;
top: -9999px;
left: -9999px;
width: 20px;
height: 20px;
}

View File

@ -1,108 +0,0 @@
.nav-bar {
@apply absolute z-10 text-white;
bottom: 10px;
left: 10px;
padding: 5px 0;
width: 35px;
height: 200px;
border-radius: 4px;
background-color: #fc8383;
box-shadow: 3px 3px 10px rgb(0 0 0 / 20%);
transition: -webkit-transform 0.7s 0.1s ease;
transition: transform 0.7s 0.1s ease;
.nav-btn {
@apply text-center;
width: 35px;
height: 24px;
line-height: 24px;
.icon {
@apply block;
margin: 2px auto;
width: 20px;
height: 20px;
}
&.active {
background-color: #5a6378;
}
}
.zoom-in .icon {
background-position: 0 -730px;
}
.zoom-out .icon {
background-position: 0 -750px;
}
.hand .icon {
@apply mx-auto my-0;
width: 25px;
height: 25px;
background-position: 0 -770px;
}
.camera .icon {
@apply mx-auto my-0;
width: 25px;
height: 25px;
background-position: 0 -870px;
}
.nav-trigger .icon {
@apply mx-auto my-0;
width: 25px;
height: 25px;
background-position: 0 -845px;
}
.zoom-pan {
@apply relative mx-auto overflow-visible bg-white;
margin: 3px auto;
width: 2px;
height: 70px;
box-shadow: 0 1px #e50000;
.origin {
@apply absolute bg-transparent;
left: -9px;
margin-top: -4px;
width: 20px;
height: 8px;
&::after {
@apply absolute block bg-white;
top: 3px;
left: 7px;
width: 6px;
height: 2px;
content: ' ';
}
}
.indicator {
@apply absolute rounded-full bg-white;
left: -3px;
margin-top: -4px;
width: 8px;
height: 8px;
}
}
}
.nav-previewer {
@apply absolute cursor-crosshair bg-white;
bottom: 30px;
left: 45px;
z-index: 9;
padding: 1px;
width: 140px;
height: 120px;
border-radius: 0 2px 2px 0;
box-shadow: 0 0 8px rgb(0 0 0 / 20%);
transition: -webkit-transform 0.7s 0.1s ease;
transition: transform 0.7s 0.1s ease;
&.grab {
@apply cursor-grabbing;
}
}

View File

@ -383,7 +383,7 @@ export interface PlanMinderNodeData extends MinderJsonNodeData {
num: number; // 关联用例数量
priority?: number; // 串行/并行
executeMethod?: RunMode; // 串行/并行值
type: PlanMinderCollectionType; // 测试集类型(功能/接口/场景)
type: PlanMinderCollectionType; // 测试集类型(功能FUNCTIONAL_CASE/接口用例API_CASE/场景SCENARIO_CASE)
extended: boolean;
grouped: boolean; // 是否使用环境组
environmentId: string;
@ -405,7 +405,6 @@ export interface PlanMinderAssociateDTO {
}
export interface PlanMinderEditListItem extends PlanMinderNodeData {
name: string;
collectionType: PlanMinderCollectionType; // 测试集类型(功能FUNCTIONAL_CASE/接口用例API_CASE/场景SCENARIO_CASE)
associateDTOS: PlanMinderAssociateDTO[];
}