feat(脑图): 脑图批量操作&公共文案

This commit is contained in:
baiqi 2024-07-31 18:17:02 +08:00 committed by 刘瑞斌
parent 658e78a098
commit 578fe9a157
20 changed files with 451 additions and 626 deletions

View File

@ -19,8 +19,14 @@
:can-show-paste-menu="!stopPaste()"
:can-show-more-menu="canShowMoreMenu()"
:can-show-priority-menu="canShowPriorityMenu()"
:custom-batch-expand="customBatchExpand"
:can-show-batch-expand="canShowBatchExpand()"
:can-show-batch-cut="true"
:can-show-batch-copy="true"
:can-show-batch-delete="true"
:priority-tooltip="t('caseManagement.caseReview.caseLevel')"
:disabled="!hasEditPermission"
can-show-more-batch-menu
single-tag
tag-enable
sequence-enable
@ -697,6 +703,26 @@
}
}
/**
* 批量展开节点
*/
function customBatchExpand(node: MinderJsonNode) {
if (node.data?.resource?.includes(caseTag)) {
expendNodeAndChildren(node);
}
}
/**
* 判断是否显示批量展开按钮
*/
function canShowBatchExpand() {
if (window.minder) {
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
return nodes.some((node) => !!node.data?.resource?.includes(caseTag));
}
return false;
}
/**
* 解析用例节点信息
* @param node 用例节点

View File

@ -1,8 +1,8 @@
import type {
InsertMenuItem,
MinderEvent,
MinderJsonNode,
MinderJsonNodeData,
MinderMenuItem,
} from '@/components/pure/ms-minder-editor/props';
import { useI18n } from '@/hooks/useI18n';
@ -50,8 +50,8 @@ export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermiss
return true;
}
const insertSiblingMenus = ref<InsertMenuItem[]>([]);
const insertSonMenus = ref<InsertMenuItem[]>([]);
const insertSiblingMenus = ref<MinderMenuItem[]>([]);
const insertSonMenus = ref<MinderMenuItem[]>([]);
/**
*
@ -236,9 +236,9 @@ export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermiss
return false;
}
if (window.minder) {
const node: MinderJsonNode = window.minder.getSelectedNode();
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
// 选中节点是用例节点时,可展示优先级菜单
return !!node?.data?.resource?.includes(caseTag);
return nodes.every((node) => !!node.data?.resource?.includes(caseTag));
}
return false;
}

View File

@ -15,6 +15,8 @@
:can-show-float-menu="canShowFloatMenu"
:can-show-delete-menu="canShowDeleteMenu"
:disabled="!hasEditPermission"
:can-show-batch-delete="true"
can-show-more-batch-menu
custom-priority
single-tag
tag-enable
@ -523,8 +525,13 @@
Message.warning(t('ms.minders.unsavedTip'));
return;
}
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (nodes.length > 1) {
extraVisible.value = false;
return;
}
extraVisible.value = !extraVisible.value;
const node: MinderJsonNode = window.minder.getSelectedNode();
const node = nodes[0];
switchingConfigFormData.value = true;
if (extraVisible.value) {
activePlanSet.value = node as PlanMinderNode;

View File

@ -7,7 +7,7 @@ import type { MinderEvent, MinderJsonNode } from '../props';
export interface UseEventListenerProps {
handleContentChange?: (node?: MinderJsonNode) => void;
handleSelectionChange?: (node?: MinderJsonNode) => void;
handleSelectionChange?: (nodes: MinderJsonNode[]) => void;
handleMinderEvent?: (event: MinderCustomEvent) => void;
handleBeforeExecCommand?: (event: MinderEvent) => void;
handleViewChange?: (event: MinderEvent) => void;
@ -36,16 +36,16 @@ export default function useEventListener(listener: UseEventListenerProps) {
minder.on(
'selectionchange',
debounce(() => {
const node: MinderJsonNode = minder.getSelectedNode();
const nodes: MinderJsonNode[] = minder.getSelectedNodes();
// 如果节点选中后即刻进行拖拽,则等待拖拽结束后再触发选中事件
if (isDragging.value) {
selectionchangeEvent = () => {
if (listener.handleSelectionChange) {
listener.handleSelectionChange(node);
listener.handleSelectionChange(nodes);
}
};
} else if (listener.handleSelectionChange) {
listener.handleSelectionChange(node);
listener.handleSelectionChange(nodes);
}
}, 300)
);

View File

@ -1,10 +1,14 @@
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor';
import { getGenerateId } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
import type { MinderJsonNode } from '../props';
import { markDeleteNode, resetNodes } from '../script/tool/utils';
import { markDeleteNode, resetNodes, setPriorityView } from '../script/tool/utils';
interface IData {
getRegisterProtocol(protocol: string): {
@ -15,16 +19,23 @@ interface IData {
export interface MinderOperationProps {
insertNode?: (node: MinderJsonNode, command: string, value?: string) => void;
customBatchExpand?: (node: MinderJsonNode) => void;
disabled?: boolean;
canShowMoreMenu?: boolean;
canShowMoreMenuNodeOperation?: boolean;
canShowPasteMenu?: boolean;
canShowDeleteMenu?: boolean;
customPriority?: boolean;
priorityStartWithZero?: boolean;
priorityPrefix?: string;
canShowBatchCut?: boolean;
canShowBatchCopy?: boolean;
canShowBatchDelete?: boolean;
}
export default function useMinderOperation({
insertNode,
canShowMoreMenuNodeOperation,
canShowPasteMenu,
}: MinderOperationProps) {
export default function useMinderOperation(options: MinderOperationProps) {
const minderStore = useMinderStore();
const { t } = useI18n();
function encode(nodes: Array<MinderJsonNode>): string {
const { editor } = window;
@ -42,8 +53,8 @@ export default function useMinderOperation({
/**
*
*/
const minderCopy = (e?: ClipboardEvent) => {
if (!canShowMoreMenuNodeOperation) {
const minderCopy = async (e?: ClipboardEvent) => {
if ((!options.canShowMoreMenu || !options.canShowMoreMenuNodeOperation) && options.canShowBatchCopy === false) {
e?.preventDefault();
return;
}
@ -57,8 +68,15 @@ export default function useMinderOperation({
case 'normal': {
const selectedNodes = minder.getSelectedNodes();
minderStore.dispatchEvent(MinderEventName.COPY_NODE, undefined, undefined, undefined, selectedNodes);
if (e?.clipboardData) {
e.clipboardData.setData('text/plain', encode(selectedNodes));
} else {
const { copy } = useClipboard();
await copy(encode(selectedNodes));
}
minder.execCommand('Copy');
e?.preventDefault();
Message.success(t('common.copySuccess'));
break;
}
default:
@ -68,8 +86,11 @@ export default function useMinderOperation({
/**
*
*/
const minderCut = (e?: ClipboardEvent) => {
if (!canShowMoreMenuNodeOperation) {
const minderCut = async (e?: ClipboardEvent) => {
if (
(options.disabled || !options.canShowMoreMenu || !options.canShowMoreMenuNodeOperation) &&
options.canShowBatchCut === false
) {
e?.preventDefault();
return;
}
@ -79,9 +100,7 @@ export default function useMinderOperation({
e?.preventDefault();
return;
}
const state = fsm.state();
switch (state) {
case 'input': {
break;
@ -90,11 +109,17 @@ export default function useMinderOperation({
markDeleteNode(minder);
const selectedNodes = minder.getSelectedNodes();
if (selectedNodes.length) {
e?.clipboardData?.setData('text/plain', encode(selectedNodes));
if (e?.clipboardData) {
e.clipboardData.setData('text/plain', encode(selectedNodes));
} else {
const { copy } = useClipboard();
await copy(encode(selectedNodes));
}
minder.execCommand('Cut');
}
e?.preventDefault();
minderStore.dispatchEvent(MinderEventName.CUT_NODE, undefined, undefined, undefined, selectedNodes);
e?.preventDefault();
Message.success(t('common.cutSuccess'));
break;
}
default:
@ -104,8 +129,8 @@ export default function useMinderOperation({
/**
*
*/
const minderPaste = (e?: ClipboardEvent) => {
if (!canShowMoreMenuNodeOperation && !canShowPasteMenu) {
const minderPaste = async (e?: ClipboardEvent) => {
if (options.disabled || !options.canShowPasteMenu) {
e?.preventDefault();
return;
}
@ -117,10 +142,8 @@ export default function useMinderOperation({
e?.preventDefault();
return;
}
const state = fsm.state();
const textData = e?.clipboardData?.getData('text/plain');
const textData = e?.clipboardData ? e.clipboardData.getData('text/plain') : await navigator.clipboard.readText();
switch (state) {
case 'input': {
// input状态下如果格式为application/km则不进行paste操作
@ -181,8 +204,8 @@ export default function useMinderOperation({
*/
const execInsertCommand = (command: string, value?: string) => {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (insertNode) {
insertNode(node, command, value);
if (options.insertNode) {
options.insertNode(node, command, value);
return;
}
if (window.minder.queryCommandState(command) !== -1) {
@ -220,13 +243,57 @@ export default function useMinderOperation({
minderStore.dispatchEvent(MinderEventName.INSERT_SIBLING, value, undefined, undefined, selectedNodes);
};
/**
*
* @param selectedNodes
*/
const minderExpand = (selectedNodes: MinderJsonNode[]) => {
if (selectedNodes.every((node) => node.isExpanded())) {
// 选中的节点集合全部展开,则全部收起
selectedNodes.forEach((node) => {
node.collapse();
node.renderTree();
});
if (!options.customPriority) {
// 展开后,需要设置一次优先级展示,避免展开后优先级显示成脑图内置文案;如果设置了自定义优先级,则不在此设置,由外部自行处理
setPriorityView(!!options.priorityStartWithZero, options.priorityPrefix || '');
window.minder.refresh();
}
minderStore.dispatchEvent(MinderEventName.COLLAPSE, undefined, undefined, undefined, selectedNodes);
} else {
// 选中的节点集合中有一个节点未展开,则全部展开
selectedNodes.forEach((node) => {
if (selectedNodes.length > 1 && options.customBatchExpand) {
// 批量操作节点才执行customBatchExpand
options.customBatchExpand(node);
} else {
node.expand();
node.renderTree();
}
});
if (!options.customPriority) {
// 展开后,需要设置一次优先级展示,避免展开后优先级显示成脑图内置文案;如果设置了自定义优先级,则不在此设置,由外部自行处理
setPriorityView(!!options.priorityStartWithZero, options.priorityPrefix || '');
window.minder.refresh();
}
minderStore.dispatchEvent(MinderEventName.EXPAND, undefined, undefined, undefined, selectedNodes);
}
};
/**
*
* @param selectedNodes
*/
const minderDelete = (selectedNodes: MinderJsonNode[]) => {
if (
(options.canShowDeleteMenu ||
(options.canShowMoreMenu && options.canShowMoreMenuNodeOperation) ||
options.canShowBatchDelete) &&
!options.disabled
) {
minderStore.dispatchEvent(MinderEventName.DELETE_NODE, undefined, undefined, undefined, selectedNodes);
window.minder.execCommand('RemoveNode');
}
};
return {
@ -236,5 +303,6 @@ export default function useMinderOperation({
appendChildNode,
appendSiblingNode,
minderDelete,
minderExpand,
};
}

View File

@ -0,0 +1,57 @@
import { isDisableNode, setPriorityView } from '../script/tool/utils';
export interface PriorityProps {
priorityStartWithZero: boolean;
priorityPrefix: string;
customPriority?: boolean;
priorityDisableCheck?: (node: any) => boolean;
}
export default function usePriority(options: PriorityProps) {
const priorityDisabled = ref(true);
function isDisable(): boolean {
if (Object.keys(window.minder).length === 0) return true;
nextTick(() => {
setPriorityView(options.priorityStartWithZero, options.priorityPrefix);
});
const node = window.minder.getSelectedNode();
if (isDisableNode(window.minder) || !node || node.parent === null) {
return true;
}
if (options.priorityDisableCheck) {
return options.priorityDisableCheck(node);
}
return !!window.minder.queryCommandState && window.minder.queryCommandState('priority') === -1;
}
function setPriority(value?: string) {
if (value && !priorityDisabled.value) {
window.minder.execCommand('priority', value);
setPriorityView(options.priorityStartWithZero, options.priorityPrefix);
} else if (window.minder.execCommand && !priorityDisabled.value) {
window.minder.execCommand('priority');
}
}
onMounted(() => {
nextTick(() => {
const freshFuc = setPriorityView;
if (window.minder && !options.customPriority) {
window.minder.on('contentchange', () => {
// 异步执行,否则执行完,还会被重置
setTimeout(() => {
freshFuc(options.priorityStartWithZero, options.priorityPrefix);
}, 0);
});
window.minder.on('selectionchange', () => {
priorityDisabled.value = isDisable();
});
}
});
});
return {
priorityDisabled,
setPriority,
};
}

View File

@ -29,12 +29,14 @@
<slot name="extractMenu"></slot>
</template>
</nodeFloatMenu>
<batchMenu v-bind="props" />
</div>
</template>
<script lang="ts" name="minderContainer" setup>
import { cloneDeep } from 'lodash-es';
import batchMenu from '../menu/batchMenu.vue';
import nodeFloatMenu from '../menu/nodeFloatMenu.vue';
import minderHeader from './header.vue';
import Navigator from './navigator.vue';
@ -47,6 +49,7 @@
import useEventListener from '../hooks/useMinderEventListener';
import {
batchMenuProps,
editMenuProps,
floatMenuProps,
headerProps,
@ -68,6 +71,7 @@
...mainEditorProps,
...tagProps,
...priorityProps,
...batchMenuProps,
});
const emit = defineEmits<{
(e: 'save', data: MinderJson, callback: () => void): void;

View File

@ -0,0 +1,188 @@
<template>
<div v-if="batchMenuVisible" class="ms-minder-node-float-menu ms-minder-batch-menu">
<a-dropdown
v-if="props.priorityCount && props.canShowPriorityMenu"
v-model:popup-visible="priorityMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleMinderMenuSelect('priority', val as string)"
>
<a-tooltip :content="props.priorityTooltip" :disabled="!props.priorityTooltip">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[priorityMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<div
class="h-[16px] w-[16px] rounded-full bg-[rgb(var(--primary-5))] text-center text-[12px] font-medium text-white"
>
P
</div>
</MsButton>
</a-tooltip>
<template #content>
<div v-if="props.priorityTooltip" class="mx-[6px] px-[8px] py-[3px] text-[var(--color-text-4)]">
{{ props.priorityTooltip }}
</div>
<template v-for="(item, pIndex) in priorityCount + 1" :key="item">
<a-doption v-if="pIndex != 0" :value="pIndex">
<div
class="flex h-[20px] w-[20px] items-center justify-center rounded-full text-[12px] font-medium text-white"
:style="{
backgroundColor: priorityColorMap[pIndex],
}"
>
{{ priorityPrefix }}{{ priorityStartWithZero ? pIndex - 1 : pIndex }}
</div>
</a-doption>
</template>
</template>
</a-dropdown>
<slot name="batchMenu"></slot>
<a-dropdown
v-if="props.canShowMoreBatchMenu"
v-model:popup-visible="moreMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, -4]"
position="tl"
trigger="click"
@select="(val) => handleMinderMenuSelect(val)"
>
<a-tooltip :content="t('common.more')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[moreMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<MsIcon type="icon-icon_more_outlined" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<a-doption v-if="props.canShowBatchCopy" value="copy">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.copy') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + C)</div>
</div>
</a-doption>
<a-doption v-if="props.canShowBatchCut" value="cut">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.cut') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + X)</div>
</div>
</a-doption>
<a-doption v-if="props.canShowBatchDelete" value="delete">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.delete') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Backspace)</div>
</div>
</a-doption>
<a-doption v-if="props.canShowBatchExpand" value="expand">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.expand') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(/)</div>
</div>
</a-doption>
</template>
</a-dropdown>
</div>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { useMinderStore } from '@/store';
import { MinderEventName } from '@/enums/minderEnum';
import useMinderOperation from '../hooks/useMinderOperation';
import usePriority from '../hooks/useMinderPriority';
import {
batchMenuProps,
floatMenuProps,
mainEditorProps,
MinderJsonNode,
priorityColorMap,
priorityProps,
} from '../props';
const props = defineProps({
...mainEditorProps,
...floatMenuProps,
...priorityProps,
...batchMenuProps,
});
const { t } = useI18n();
const minderStore = useMinderStore();
const { setPriority } = usePriority(props);
const batchMenuVisible = ref(false);
const priorityMenuVisible = ref(false);
const moreMenuVisible = ref(false);
const { minderCopy, minderCut, minderDelete, minderExpand } = useMinderOperation(props);
watch(
() => minderStore.event.eventId,
async () => {
if (window.minder) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (
minderStore.event.name === MinderEventName.DRAG_FINISH ||
minderStore.event.name === MinderEventName.NODE_UNSELECT
) {
batchMenuVisible.value = selectedNodes.length > 1;
}
}
},
{
immediate: true,
}
);
/**
* 处理快捷菜单选择
* @param type 选择的菜单项
*/
function handleMinderMenuSelect(type: string | number | Record<string, any> | undefined, value?: string) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (selectedNodes.length > 0) {
switch (type) {
case 'copy':
minderCopy();
break;
case 'cut':
minderCut();
break;
case 'expand':
minderExpand(selectedNodes);
break;
case 'delete':
minderDelete(selectedNodes);
break;
case 'priority':
setPriority(value);
break;
default:
break;
}
}
}
</script>
<style lang="less" scoped>
.ms-minder-batch-menu {
@apply absolute flex w-auto flex-1 items-center bg-white;
bottom: 6px;
left: 50%;
padding: 4px 8px;
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
gap: 8px;
}
</style>

View File

@ -137,32 +137,32 @@
<a-doption v-if="props.canShowEnterNode" value="enterNode">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.enterNode') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl+ Enter)</div> -->
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl+ Enter)</div>
</div>
</a-doption>
<template v-if="props.canShowMoreMenuNodeOperation">
<a-doption value="copy">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.copy') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + C)</div> -->
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + C)</div>
</div>
</a-doption>
<a-doption value="cut">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.cut') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + X)</div> -->
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + X)</div>
</div>
</a-doption>
<a-doption v-if="props.canShowPasteMenu && minderStore.clipboard.length > 0" value="paste">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.paste') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + V)</div> -->
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + V)</div>
</div>
</a-doption>
<a-doption value="delete">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.delete') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Backspace)</div> -->
<div class="ml-[4px] text-[var(--color-text-4)]">(Backspace)</div>
</div>
</a-doption>
</template>
@ -200,8 +200,9 @@
import { MinderEventName } from '@/enums/minderEnum';
import useMinderOperation from '../hooks/useMinderOperation';
import { floatMenuProps, mainEditorProps, MinderJsonNode, priorityProps, tagProps } from '../props';
import { isDisableNode, isNodeInMinderView, setPriorityView } from '../script/tool/utils';
import usePriority from '../hooks/useMinderPriority';
import { floatMenuProps, mainEditorProps, MinderJsonNode, priorityColorMap, priorityProps, tagProps } from '../props';
import { isNodeInMinderView } from '../script/tool/utils';
const props = defineProps({
...mainEditorProps,
@ -215,6 +216,7 @@
const { t } = useI18n();
const minderStore = useMinderStore();
const { setPriority } = usePriority(props);
const currentNodeTags = ref<string[]>([]);
const tags = ref<string[]>([]);
@ -240,7 +242,7 @@
}
}
if (selectedNodes.length > 1) {
// TODO:
//
menuVisible.value = false;
return;
}
@ -304,31 +306,8 @@
}
}
const priorityColorMap: Record<number, string> = {
1: 'rgb(var(--danger-6))',
2: 'rgb(var(--link-6))',
3: 'rgb(var(--success-6))',
4: 'rgb(var(--warning-6))',
};
const priorityDisabled = ref(true);
function isDisable(): boolean {
if (Object.keys(window.minder).length === 0) return true;
nextTick(() => {
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
});
const node = window.minder.getSelectedNode();
if (isDisableNode(window.minder) || !node || node.parent === null) {
return true;
}
if (props.priorityDisableCheck) {
return props.priorityDisableCheck(node);
}
return !!window.minder.queryCommandState && window.minder.queryCommandState('priority') === -1;
}
const { minderCopy, minderCut, minderPaste, appendChildNode, appendSiblingNode, minderDelete } = useMinderOperation({
insertNode: props.insertNode,
});
const { minderCopy, minderCut, minderPaste, appendChildNode, appendSiblingNode, minderDelete } =
useMinderOperation(props);
/**
* 处理快捷菜单选择
@ -360,12 +339,7 @@
minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, undefined, [selectedNodes[0]]);
break;
case 'priority':
if (value && !priorityDisabled.value) {
window.minder.execCommand('priority', value);
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
} else if (window.minder.execCommand && !priorityDisabled.value) {
window.minder.execCommand('priority');
}
setPriority(value);
break;
default:
break;
@ -385,23 +359,6 @@
}
}
);
onMounted(() => {
nextTick(() => {
const freshFuc = setPriorityView;
if (window.minder && !props.customPriority) {
window.minder.on('contentchange', () => {
//
setTimeout(() => {
freshFuc(props.priorityStartWithZero, props.priorityPrefix);
}, 0);
});
window.minder.on('selectionchange', () => {
priorityDisabled.value = isDisable();
});
}
});
});
</script>
<style lang="less">

View File

@ -1,34 +0,0 @@
<template>
<div class="arrange-group">
<div class="arrange menu-btn" :disabled="disabled" @click="resetlayout">
<span class="tab-icons" />
<span class="label">
{{ t('minder.menu.arrange.arrange_layout') }}
</span>
</div>
</div>
</template>
<script lang="ts" name="Arrange" setup>
import { computed } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const disabled = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState && window.minder.queryCommandState('resetlayout') === -1;
});
function resetlayout() {
if (window.minder.queryCommandState('resetlayout') !== -1) {
window.minder.execCommand('resetlayout');
}
}
</script>

View File

@ -1,261 +0,0 @@
<template>
<div class="font-group ml-[10px]">
<a-select
v-model="fontFamilyDefaultValue"
:placeholder="t('minder.menu.font.font')"
class="font-family-select"
:disabled="disabledFont"
size="mini"
@change="execCommandFontFamily"
>
<a-option
v-for="item in fontFamilys"
:key="item.id"
:label="item.name"
:value="item.value"
:style="{ 'font-family': item.value }"
/>
</a-select>
<a-select
v-model="fontSizeDefaultValue"
:placeholder="t('minder.menu.font.size')"
class="font-size-select"
:disabled="disabledFontSize"
size="mini"
@change="execCommandFontSize"
>
<a-option
v-for="item in fontSizes"
:key="item.id"
:label="item.label.toString()"
:value="item.value"
:style="{
'font-size': item.value + 'px',
'height': 2 * item.value + 'px',
'line-height': 2 * item.value + 'px',
'padding': 0,
}"
/>
</a-select>
<span class="font-btn mt-[2px]">
<span
class="menu-btn tab-icons font-bold"
:class="{ selected: boldSelected }"
:disabled="disabledBold"
@click="execCommandFontStyle('bold')"
/>
<span
class="font-italic menu-btn tab-icons"
:class="{ selected: italicSelected }"
:disabled="disabledItalic"
@click="execCommandFontStyle('italic')"
/>
</span>
</div>
</template>
<script lang="ts" name="StyleOperation" setup>
import { computed, ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const fontFamilys = [
{
id: 1,
value: '宋体,SimSun',
name: '宋体',
},
{
id: 2,
value: '微软雅黑,Microsoft YaHei',
name: '微软雅黑',
},
{
id: 3,
value: '楷体,楷体_GB2312,SimKai',
name: '楷体',
},
{
id: 4,
value: '黑体, SimHei',
name: '黑体',
},
{
id: 5,
value: '隶书, SimLi',
name: '隶书',
},
{
id: 6,
value: 'andale mono',
name: 'Andale Mono',
},
{
id: 7,
value: 'arial,helvetica,sans-serif',
name: 'Arial',
},
{
id: 8,
value: 'arial black,avant garde',
name: 'arialBlack',
},
{
id: 9,
value: 'comic sans ms',
name: 'comic Sans Ms',
},
{
id: 10,
value: 'impact,chicago',
name: 'Impact',
},
{
id: 11,
value: 'times new roman',
name: 'times New Roman',
},
{
id: 12,
value: 'sans-serif',
name: 'Sans-Serif',
},
];
const fontSizes = [
{
id: 1,
value: 10,
label: 10,
},
{
id: 2,
value: 12,
label: 12,
},
{
id: 3,
value: 16,
label: 16,
},
{
id: 4,
value: 18,
label: 18,
},
{
id: 5,
value: 24,
label: 24,
},
{
id: 6,
value: 32,
label: 32,
},
{
id: 7,
value: 48,
label: 48,
},
];
const fontFamilyDefaultValue = ref('');
const fontSizeDefaultValue = ref('');
const disabledFont = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
const currentFontFamily = window.minder.queryCommandValue('fontfamily');
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
fontFamilyDefaultValue.value = currentFontFamily || t('minder.menu.font.font');
return window.minder.queryCommandState('fontfamily') === -1;
});
const disabledFontSize = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
fontSizeDefaultValue.value = window.minder.queryCommandValue('fontsize') || t('minder.menu.font.size');
return window.minder.queryCommandState('fontsize') === -1;
});
const disabledBold = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState('bold') === -1;
});
const disabledItalic = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState('italic') === -1;
});
const boldSelected = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState('bold') === -1;
});
const italicSelected = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState('italic') === -1;
});
function execCommandFontFamily(
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
if (value === t('minder.menu.font.font')) {
return;
}
window.minder.execCommand('fontfamily', value);
}
function execCommandFontSize(
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
if (typeof value !== 'number') {
return;
}
window.minder.execCommand('fontsize', value);
}
function execCommandFontStyle(style: string) {
switch (style) {
case 'bold':
if (window.minder.queryCommandState('bold') !== -1) {
window.minder.execCommand('bold');
}
break;
case 'italic':
if (window.minder.queryCommandState('italic') !== -1) {
window.minder.execCommand('italic');
}
break;
default:
}
}
</script>

View File

@ -1,93 +0,0 @@
<template>
<a-dropdown class="toggle" :disabled="disabled" @select="handleCommand">
<span class="dropdown-toggle mold-icons menu-btn cursor-pointer" :class="'mold-' + (moldIndex + 1)" />
<template #content>
<a-doption class="dropdown-item" :value="1">
<div class="mold-icons mold-1"></div>
</a-doption>
<a-doption class="dropdown-item" :value="2">
<div class="mold-icons mold-2"></div>
</a-doption>
<a-doption class="dropdown-item" :value="3">
<div class="mold-icons mold-3"></div>
</a-doption>
<a-doption class="dropdown-item" :value="4">
<div class="mold-icons mold-4"></div>
</a-doption>
<a-doption class="dropdown-item" :value="5">
<div class="mold-icons mold-5"></div>
</a-doption>
<a-doption class="dropdown-item" :value="6">
<div class="mold-icons mold-6"></div>
</a-doption>
</template>
</a-dropdown>
</template>
<script lang="ts" name="Mold" setup>
import { computed, nextTick, onMounted, ref } from 'vue';
import useMinderStore from '@/store/modules/components/minder-editor';
import { moleProps } from '../../props';
const props = defineProps(moleProps);
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
}>();
const minderStore = useMinderStore();
const moldIndex = ref(0);
const disabled = computed(() => {
try {
if (!window.minder) return false;
} catch (e) {
// windowminderundefined
return false;
}
return window.minder.queryCommandState('template') === -1;
});
const templateList = computed(() => window.kityminder.Minder.getTemplateList());
function handleCommand(value: string | number | Record<string, any> | undefined) {
moldIndex.value = (value as number) - 1;
window.minder.execCommand('template', Object.keys(templateList.value)[(value as number) - 1]);
emit('moldChange', (value as number) - 1);
}
onMounted(() => {
nextTick(() => handleCommand(props.defaultMold));
});
</script>
<style lang="less" scoped>
:deep(.arco-dropdown-list) {
@apply grid grid-cols-2;
}
.dropdown-toggle .mold-icons,
.mold-icons {
background-image: url('@/assets/images/minder/mold.png');
background-repeat: no-repeat;
}
.dropdown-item {
@apply flex items-center justify-center;
height: 50px !important;
}
.mold-loop(@i) when (@i > 0) {
.mold-@{i} {
@apply flex;
margin-top: 5px;
width: 50px;
height: 45px;
background-position: (1 - @i) * 50px 0;
}
.mold-loop(@i - 1);
}
.mold-loop(6);
</style>

View File

@ -1,79 +0,0 @@
<template>
<div class="style-group">
<div class="clear-style-btn menu-btn" :disabled="disabled" @click="clearstyle">
<span class="tab-icons" />
<span class="label">
{{ t('minder.menu.style.clear') }}
</span>
</div>
<div class="copy-paste-panel" @click="copystyle">
<div class="copy-style menu-btn" :disabled="disabled">
<span class="tab-icons" />
<span class="label">
{{ t('minder.menu.style.copy') }}
</span>
</div>
<div class="paste-style menu-btn" :disabled="disabled" @click="pastestyle">
<span class="tab-icons" />
<span class="label">
{{ t('minder.menu.style.paste') }}
</span>
</div>
</div>
</div>
</template>
<script lang="ts" name="StyleOperation" setup>
import { nextTick, onMounted, reactive, ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
let minder = reactive<any>({});
const disabled = ref(true);
function checkDisabled() {
try {
if (Object.keys(minder).length === 0) return false;
} catch (e) {
// windowminderundefined
return false;
}
const nodes = minder.getSelectedNodes && minder.getSelectedNodes();
disabled.value = nodes === null || nodes.length === 0;
}
onMounted(() => {
nextTick(() => {
minder = window.minder;
minder.on('selectionchange', () => {
checkDisabled();
});
});
});
function clearstyle() {
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('clearstyle') !== -1) {
minder.execCommand('clearstyle');
}
}
function copystyle() {
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('copystyle') !== -1) {
minder.execCommand('copystyle');
}
}
function pastestyle() {
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('pastestyle') !== -1) {
minder.execCommand('pastestyle');
}
}
</script>
<style>
.mold-dropdown-list .mold-icons,
.mold-icons {
background-image: url('@/assets/images/minder/mold.png');
background-repeat: no-repeat;
}
</style>

View File

@ -1,39 +0,0 @@
<template>
<div class="menu-container">
<mold v-if="moldEnable" :default-mold="props.defaultMold" @mold-change="handleMoldChange" />
<arrange v-if="arrangeEnable" />
<style-operation v-if="styleEnable" />
<font-operation v-if="fontEnable" />
</div>
</template>
<script lang="ts" name="viewMenu" setup>
import arrange from './arrange.vue';
import fontOperation from './fontOperation.vue';
import mold from './mold.vue';
import styleOperation from './styleOperation.vue';
import { moleProps, viewMenuProps } from '../../props';
const props = defineProps({ ...moleProps, ...viewMenuProps });
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
}>();
function handleMoldChange(data: number) {
emit('moldChange', data);
}
</script>
<style lang="less" scoped>
.menu-container {
height: 60px;
i {
@apply inline-block;
width: 20px;
height: 20px;
}
}
</style>

View File

@ -37,6 +37,7 @@
import useMinderOperation from './hooks/useMinderOperation';
import useShortCut from './hooks/useShortCut';
import {
batchMenuProps,
delProps,
editMenuProps,
floatMenuProps,
@ -50,13 +51,14 @@
tagProps,
viewMenuProps,
} from './props';
import { isNodeInMinderView, setPriorityView } from './script/tool/utils';
import { isNodeInMinderView } from './script/tool/utils';
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
(e: 'save', data: MinderJson, callback: () => void): void;
(e: 'afterMount'): void;
(e: 'nodeSelect', data: MinderJsonNode): void;
(e: 'nodeBatchSelect', data: MinderJsonNode[]): void;
(e: 'contentChange', data?: MinderJsonNode): void;
(e: 'action', event: MinderCustomEvent): void;
(e: 'beforeExecCommand', event: MinderEvent): void;
@ -74,6 +76,7 @@
...tagProps,
...delProps,
...viewMenuProps,
...batchMenuProps,
});
const minderStore = useMinderStore();
@ -115,11 +118,7 @@
emit('save', data, callback);
}
const { appendChildNode, appendSiblingNode, minderDelete } = useMinderOperation({
insertNode: props.insertNode,
canShowMoreMenuNodeOperation: props.canShowMoreMenuNodeOperation,
canShowPasteMenu: props.canShowPasteMenu,
});
const { appendChildNode, appendSiblingNode, minderDelete, minderExpand } = useMinderOperation(props);
const { unbindShortcuts } = useShortCut(
{
undo: () => {
@ -135,26 +134,17 @@
}
},
delete: () => {
if (props.canShowMoreMenuNodeOperation && !props.disabled) {
if (
(props.canShowDeleteMenu || (props.canShowMoreMenu && props.canShowMoreMenuNodeOperation)) &&
!props.disabled
) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
minderDelete(selectedNodes);
}
},
expand: () => {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (selectedNodes.every((node) => node.isExpanded())) {
//
window.minder.execCommand('Collapse');
minderStore.dispatchEvent(MinderEventName.COLLAPSE, undefined, undefined, undefined, selectedNodes);
} else {
//
window.minder.execCommand('Expand');
if (!props.customPriority) {
//
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
}
minderStore.dispatchEvent(MinderEventName.EXPAND, undefined, undefined, undefined, selectedNodes);
}
minderExpand(selectedNodes);
},
appendChildNode: () => {
if (props.insertSonMenus.length > 0 || props.insertNode) {
@ -169,17 +159,18 @@
}
},
},
{
insertNode: props.insertNode,
canShowMoreMenuNodeOperation: props.canShowMoreMenuNodeOperation,
canShowPasteMenu: props.canShowPasteMenu,
}
props
);
onMounted(() => {
window.minderProps = props;
useMinderEventListener({
handleSelectionChange: (node?: MinderJsonNode) => {
handleSelectionChange: (nodes: MinderJsonNode[]) => {
if (nodes && nodes.length > 1) {
emit('nodeBatchSelect', nodes);
return;
}
const node = nodes[0];
if (node) {
emit('nodeSelect', node);
const box = node.getRenderBox();

View File

@ -102,6 +102,12 @@ export const priorityProps = {
default: '',
},
};
export const priorityColorMap: Record<number, string> = {
1: 'rgb(var(--danger-6))',
2: 'rgb(var(--link-6))',
3: 'rgb(var(--success-6))',
4: 'rgb(var(--warning-6))',
};
export interface MinderReplaceTag {
tags: string[];
@ -133,7 +139,7 @@ export const tagProps = {
afterTagEdit: Function as PropType<(nodes: MinderJsonNode[], tag: string) => void>,
};
export interface InsertMenuItem {
export interface MinderMenuItem {
value: string;
label: string;
}
@ -146,14 +152,14 @@ export interface MoreMenuOtherOperationItem {
export const floatMenuProps = {
// 插入同级选项
insertSiblingMenus: {
type: Array as PropType<InsertMenuItem[]>,
type: Array as PropType<MinderMenuItem[]>,
default() {
return [];
},
},
// 插入子级选项
insertSonMenus: {
type: Array as PropType<InsertMenuItem[]>,
type: Array as PropType<MinderMenuItem[]>,
default() {
return [];
},
@ -210,6 +216,31 @@ export const floatMenuProps = {
default: false,
},
};
export const batchMenuProps = {
canShowMoreBatchMenu: {
type: Boolean,
default: false,
},
canShowBatchCopy: {
type: Boolean,
default: false,
},
canShowBatchCut: {
type: Boolean,
default: false,
},
canShowBatchDelete: {
type: Boolean,
default: false,
},
canShowBatchExpand: {
type: Boolean,
default: false,
},
customBatchExpand: {
type: Function as PropType<(node: MinderJsonNode) => void>,
},
};
export const editMenuProps = {
sequenceEnable: {

View File

@ -201,4 +201,5 @@ export default {
'common.jump': 'Jump',
'common.gotIt': 'Got it',
'common.inputPleaseEnterTags': 'Please enter the update tag enter add, not more than 64 characters',
'common.cutSuccess': 'Cut successfully',
};

View File

@ -200,4 +200,5 @@ export default {
'common.jump': '跳转',
'common.gotIt': '知道了',
'common.inputPleaseEnterTags': '请输入更新标签回车添加不得超过64字符',
'common.cutSuccess': '剪切成功',
};

View File

@ -1,5 +1,5 @@
export default {
'login.form.title': 'Modern, open-source test management and interface testing tools',
'login.form.title': 'Open Source Continuous Testing Tools',
'login.form.userName.errMsg': 'Username cannot be empty',
'login.form.password.errMsg': 'Password cannot be empty',
'login.form.login.errMsg': 'Login error, refresh and try again',

View File

@ -1,5 +1,5 @@
export default {
'login.form.title': '现代化、开源的测试管理和接口测试工具',
'login.form.title': '开源持续测试工具',
'login.form.userName.errMsg': '用户名不能为空',
'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错,请刷新重试',