feat(脑图): 快捷键优化&交互优化

This commit is contained in:
baiqi 2024-09-05 18:06:46 +08:00 committed by Craftsman
parent 40445cb4c5
commit 748b4e93e4
23 changed files with 247 additions and 247 deletions

View File

@ -13,6 +13,7 @@
:can-show-enter-node="canShowEnterNode" :can-show-enter-node="canShowEnterNode"
:can-show-more-menu-node-operation="false" :can-show-more-menu-node-operation="false"
:more-menu-other-operation-list="canShowFloatMenu ? moreMenuOtherOperationList : []" :more-menu-other-operation-list="canShowFloatMenu ? moreMenuOtherOperationList : []"
:shortcut-list="['expand']"
disabled disabled
@node-select="handleNodeSelect" @node-select="handleNodeSelect"
@node-unselect="handleNodeUnselect" @node-unselect="handleNodeUnselect"
@ -93,6 +94,16 @@
/> />
</div> </div>
</template> </template>
<template #shortCutList>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.pass') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> P </div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.unPass') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> R </div>
</div>
</template>
</MsMinderEditor> </MsMinderEditor>
</div> </div>
</template> </template>
@ -191,7 +202,7 @@
data: { data: {
...e.data, ...e.data,
id: e.id || e.data?.id || '', id: e.id || e.data?.id || '',
text: e.name || e.data?.text || '', text: e.name || e.data?.text.replace(/<\/?p\b[^>]*>/gi, '') || '',
resource: modulesCount.value[e.id] !== undefined ? [moduleTag] : e.data?.resource, resource: modulesCount.value[e.id] !== undefined ? [moduleTag] : e.data?.resource,
expandState: e.level === 0 ? 'expand' : 'collapse', expandState: e.level === 0 ? 'expand' : 'collapse',
count: modulesCount.value[e.id], count: modulesCount.value[e.id],

View File

@ -68,6 +68,28 @@
<caseCommentList v-else-if="activeExtraKey === 'comments'" :active-case="activeCase" /> <caseCommentList v-else-if="activeExtraKey === 'comments'" :active-case="activeCase" />
<bugList v-else :active-case="activeCase" /> <bugList v-else :active-case="activeCase" />
</template> </template>
<template #shortCutList>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('ms.minders.createSiblingModule') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> M </div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('ms.minders.createSiblingCase') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> C </div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('ms.minders.createChildModule') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto">
Shift + M
</div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('ms.minders.createChildCase') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto">
Shift + C
</div>
</div>
</template>
</MsMinderEditor> </MsMinderEditor>
</div> </div>
</template> </template>
@ -76,6 +98,7 @@
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import useShortCut from '@/components/pure/ms-minder-editor/hooks/useShortCut';
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue'; import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import type { MinderJson, MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props'; import type { MinderJson, MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
import { import {
@ -181,7 +204,7 @@
data: { data: {
...e.data, ...e.data,
id: e.id || e.data?.id || '', id: e.id || e.data?.id || '',
text: e.name || e.data?.text || '', text: e.name || e.data?.text.replace(/<\/?p\b[^>]*>/gi, '') || '',
resource: props.modulesCount[e.id] !== undefined ? [moduleTag] : e.data?.resource, resource: props.modulesCount[e.id] !== undefined ? [moduleTag] : e.data?.resource,
expandState: e.level === 0 ? 'expand' : 'collapse', expandState: e.level === 0 ? 'expand' : 'collapse',
count: props.modulesCount[e.id], count: props.modulesCount[e.id],
@ -634,6 +657,28 @@
setPriorityView(true, 'P'); setPriorityView(true, 'P');
} }
const { unbindShortcuts } = useShortCut(
{
addChildModule: () => {
const node: MinderJsonNode = window.minder.getSelectedNode();
insertNode(node, 'AppendChildNode', moduleTag);
},
addChildCase: () => {
const node: MinderJsonNode = window.minder.getSelectedNode();
insertNode(node, 'AppendChildNode', caseTag);
},
addSiblingModule: () => {
const node: MinderJsonNode = window.minder.getSelectedNode();
insertNode(node, 'AppendSiblingNode', moduleTag);
},
addSiblingCase: () => {
const node: MinderJsonNode = window.minder.getSelectedNode();
insertNode(node, 'AppendSiblingNode', caseTag);
},
},
{}
);
/** /**
* 标签编辑后如果将标签修改为模块则删除已添加的优先级 * 标签编辑后如果将标签修改为模块则删除已添加的优先级
* @param node 选中节点 * @param node 选中节点
@ -953,6 +998,10 @@
deep: true, deep: true,
} }
); );
onBeforeUnmount(() => {
unbindShortcuts();
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -1,13 +0,0 @@
export default {
'ms.minders.allModule': 'All Modules',
'ms.minders.precondition': 'Pre-condition',
'ms.minders.stepDesc': 'Step Description',
'ms.minders.stepExpect': 'Expected Result',
'ms.minders.textDesc': 'Text Description',
'ms.minders.remark': 'Remark Information',
'ms.minders.caseName': 'Test Case Name',
'ms.minders.caseNameNotNull': 'Test Case Name cannot be empty',
'ms.minders.commentTotal': '{num} Comments in Total',
'ms.minders.text': 'Text',
'ms.minders.leaveUnsavedTip': 'The mind map has unsaved changes, are you sure you want to exit?',
};

View File

@ -1,13 +0,0 @@
export default {
'ms.minders.allModule': '全部模块',
'ms.minders.precondition': '前置条件',
'ms.minders.stepDesc': '步骤描述',
'ms.minders.stepExpect': '预期结果',
'ms.minders.textDesc': '文本描述',
'ms.minders.remark': '备注信息',
'ms.minders.caseName': '用例名称',
'ms.minders.caseNameNotNull': '用例名称不能为空',
'ms.minders.commentTotal': '共 {num} 评论',
'ms.minders.text': '文本',
'ms.minders.leaveUnsavedTip': '脑图有未保存的更改,确认离开吗?',
};

View File

@ -1,3 +1,4 @@
import usePriority from '@/components/pure/ms-minder-editor/hooks/useMinderPriority';
import type { import type {
MinderEvent, MinderEvent,
MinderJsonNode, MinderJsonNode,
@ -16,6 +17,7 @@ import { getGenerateId } from '@/utils';
export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermission?: boolean }) { export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermission?: boolean }) {
const { t } = useI18n(); const { t } = useI18n();
const minderStore = useMinderStore(); const minderStore = useMinderStore();
const { setPriority } = usePriority({ priorityStartWithZero: true, priorityPrefix: 'P' });
const caseTag = t('common.case'); const caseTag = t('common.case');
const moduleTag = t('common.module'); const moduleTag = t('common.module');
@ -361,13 +363,33 @@ export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermiss
* @param value * @param value
*/ */
function insertSpecifyNode(type: string, value: string) { function insertSpecifyNode(type: string, value: string) {
const nodeId = getGenerateId();
execInert(type, { execInert(type, {
id: getGenerateId(), id: nodeId,
text: value !== t('ms.minders.text') ? value : '', text: value !== t('ms.minders.text') ? value : '',
resource: value !== t('ms.minders.text') ? [value] : [], resource: value !== t('ms.minders.text') ? [value] : [],
expandState: 'expand', expandState: 'expand',
isNew: true, isNew: true,
priority: value === caseTag ? 1 : undefined,
}); });
if (value === caseTag) {
// 用例节点插入后,插入子节点
setPriority('1');
nextTick(() => {
insertSpecifyNode('AppendChildNode', prerequisiteTag);
// 上面插入了子节点前置条件后会选中该节点,所以下面插入同级
insertSpecifyNode('AppendSiblingNode', stepTag);
insertSpecifyNode('AppendChildNode', stepExpectTag);
window.minder.selectById(nodeId);
insertSpecifyNode('AppendChildNode', remarkTag);
nextTick(() => {
// 取消选中备注节点,选中用例节点
const remarkNode: MinderJsonNode = window.minder.getSelectedNode();
window.minder.toggleSelect(remarkNode);
window.minder.selectById(nodeId);
});
});
}
} }
/** /**

View File

@ -1,4 +1,21 @@
export default { export default {
// 功能用例脑图文案
'ms.minders.allModule': 'All Modules',
'ms.minders.precondition': 'Pre-condition',
'ms.minders.stepDesc': 'Step Description',
'ms.minders.stepExpect': 'Expected Result',
'ms.minders.textDesc': 'Text Description',
'ms.minders.remark': 'Remark Information',
'ms.minders.caseName': 'Test Case Name',
'ms.minders.caseNameNotNull': 'Test Case Name cannot be empty',
'ms.minders.commentTotal': '{num} Comments in Total',
'ms.minders.text': 'Text',
'ms.minders.leaveUnsavedTip': 'The mind map has unsaved changes, are you sure you want to exit?',
'ms.minders.createSiblingModule': 'Insert sibling Module',
'ms.minders.createSiblingCase': 'Insert sibling Case',
'ms.minders.createChildModule': 'Insert child Module',
'ms.minders.createChildCase': 'Insert child Case',
// 测试规划脑图文案
'ms.minders.failStop': 'Failure stop', 'ms.minders.failStop': 'Failure stop',
'ms.minders.failRetry': 'Retry on failure', 'ms.minders.failRetry': 'Retry on failure',
'ms.minders.stepRetry': 'Step Retry', 'ms.minders.stepRetry': 'Step Retry',

View File

@ -1,4 +1,21 @@
export default { export default {
// 功能用例脑图文案
'ms.minders.allModule': '全部模块',
'ms.minders.precondition': '前置条件',
'ms.minders.stepDesc': '步骤描述',
'ms.minders.stepExpect': '预期结果',
'ms.minders.textDesc': '文本描述',
'ms.minders.remark': '备注信息',
'ms.minders.caseName': '用例名称',
'ms.minders.caseNameNotNull': '用例名称不能为空',
'ms.minders.commentTotal': '共 {num} 评论',
'ms.minders.text': '文本',
'ms.minders.leaveUnsavedTip': '脑图有未保存的更改,确认离开吗?',
'ms.minders.createSiblingModule': '添加同级模块',
'ms.minders.createSiblingCase': '添加同级用例',
'ms.minders.createChildModule': '添加子级模块',
'ms.minders.createChildCase': '添加子级用例',
// 测试规划脑图文案
'ms.minders.failStop': '失败停止', 'ms.minders.failStop': '失败停止',
'ms.minders.failRetry': '失败重试', 'ms.minders.failRetry': '失败重试',
'ms.minders.stepRetry': '步骤重试', 'ms.minders.stepRetry': '步骤重试',

View File

@ -13,6 +13,7 @@
:can-show-enter-node="canShowEnterNode" :can-show-enter-node="canShowEnterNode"
:can-show-more-menu-node-operation="false" :can-show-more-menu-node-operation="false"
:more-menu-other-operation-list="canShowFloatMenu && hasOperationPermission ? moreMenuOtherOperationList : []" :more-menu-other-operation-list="canShowFloatMenu && hasOperationPermission ? moreMenuOtherOperationList : []"
:shortcut-list="['expand']"
disabled disabled
@node-batch-select="handleNodeBatchSelect" @node-batch-select="handleNodeBatchSelect"
@node-select="handleNodeSelect" @node-select="handleNodeSelect"
@ -134,6 +135,20 @@
</template> </template>
</a-dropdown> </a-dropdown>
</template> </template>
<template #shortCutList>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.success') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> S </div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.fail') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> E </div>
</div>
<div class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.block') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> B </div>
</div>
</template>
</MsMinderEditor> </MsMinderEditor>
<LinkDefectDrawer <LinkDefectDrawer
v-if="isMinderOperation" v-if="isMinderOperation"
@ -302,7 +317,7 @@
...e.data, ...e.data,
type: e.type || e.data?.type, type: e.type || e.data?.type,
id: e.id || e.data?.id || '', id: e.id || e.data?.id || '',
text: e.name || e.data?.text || '', text: e.name || e.data?.text.replace(/<\/?p\b[^>]*>/gi, '') || '',
resource: modulesCount.value[e.id] !== undefined ? [moduleTag] : e.data?.resource, resource: modulesCount.value[e.id] !== undefined ? [moduleTag] : e.data?.resource,
expandState: e.level === 0 ? 'expand' : 'collapse', expandState: e.level === 0 ? 'expand' : 'collapse',
count: modulesCount.value[e.id], count: modulesCount.value[e.id],
@ -608,14 +623,18 @@
); );
if (actualResultNode) { if (actualResultNode) {
if (content.length) { if (content.length) {
actualResultNode.setData('text', content).render(); actualResultNode.setData('text', content.replace(/<\/?p\b[^>]*>/gi, '')).render();
} else { } else {
// //
window.minder.removeNode(actualResultNode); window.minder.removeNode(actualResultNode);
} }
} else if (content.length) { } else if (content.length) {
actualResultNode = createNode( actualResultNode = createNode(
{ resource: [actualResultTag], text: content, id: `actualResult-${node.data?.id}` }, {
resource: [actualResultTag],
text: content.replace(/<\/?p\b[^>]*>/gi, ''),
id: `actualResult-${node.data?.id}`,
},
node node
); );
handleRenderNode(node, [actualResultNode]); handleRenderNode(node, [actualResultNode]);
@ -983,6 +1002,9 @@
[executionResultMap.ERROR.statusText]: 5, [executionResultMap.ERROR.statusText]: 5,
[executionResultMap.BLOCKED.statusText]: 6, [executionResultMap.BLOCKED.statusText]: 6,
}; };
});
onBeforeUnmount(() => {
unbindShortcuts(); unbindShortcuts();
}); });

View File

@ -19,6 +19,7 @@
:can-show-dropdown="canShowDropdown" :can-show-dropdown="canShowDropdown"
:dropdown-list="dropdownList" :dropdown-list="dropdownList"
:checked-val="checkedVal" :checked-val="checkedVal"
:shortcut-list="['expand', 'addSibling', 'addChild', 'delete']"
custom-priority custom-priority
single-tag single-tag
tag-enable tag-enable

View File

@ -1,7 +1,7 @@
import { isMacOs } from '@/utils'; import { isMacOs } from '@/utils';
export default defineComponent(() => { export default defineComponent((props: { size?: number }) => {
const isMac = isMacOs(); const isMac = isMacOs();
return () => (isMac ? <icon-command size={14} /> : 'Ctrl'); return () => (isMac ? <icon-command size={props.size || 14} /> : 'Ctrl');
}); });

View File

@ -2,6 +2,7 @@ import type { MinderJsonNode } from '../props';
import useMinderOperation, { type MinderOperationProps } from './useMinderOperation'; import useMinderOperation, { type MinderOperationProps } from './useMinderOperation';
type ShortcutKey = type ShortcutKey =
| 'save'
| 'expand' | 'expand'
| 'enter' | 'enter'
| 'appendSiblingNode' | 'appendSiblingNode'
@ -11,7 +12,11 @@ type ShortcutKey =
| 'delete' | 'delete'
| 'executeToSuccess' | 'executeToSuccess'
| 'executeToBlocked' | 'executeToBlocked'
| 'executeToError'; | 'executeToError'
| 'addChildCase'
| 'addChildModule'
| 'addSiblingModule'
| 'addSiblingCase';
// 快捷键事件映射combinationShortcuts中定义了组合键事件key为组合键value为事件名称 // 快捷键事件映射combinationShortcuts中定义了组合键事件key为组合键value为事件名称
type Shortcuts = { type Shortcuts = {
[key in ShortcutKey]?: () => void; [key in ShortcutKey]?: () => void;
@ -38,12 +43,14 @@ export default function useShortCut(shortcuts: Shortcuts, options: MinderOperati
} }
const key = event.key.toLowerCase(); const key = event.key.toLowerCase();
const isCtrlOrCmd = event.ctrlKey || event.metaKey; const isCtrlOrCmd = event.ctrlKey || event.metaKey;
const isShift = event.shiftKey;
// 定义组合键事件 // 定义组合键事件
const combinationShortcuts: { [key: string]: ShortcutKey } = { const combinationShortcuts: { [key: string]: ShortcutKey } = {
// z: 'undo', // 撤销 TODO:暂时不上撤销和重做 // z: 'undo', // 撤销 TODO:暂时不上撤销和重做
// y: 'redo', // 重做 // y: 'redo', // 重做
enter: 'enter', // 进入节点 enter: 'enter', // 进入节点
save: 'save', // 保存
}; };
// 定义单键事件 // 定义单键事件
const singleShortcuts: { [key: string]: ShortcutKey } = { const singleShortcuts: { [key: string]: ShortcutKey } = {
@ -57,12 +64,21 @@ export default function useShortCut(shortcuts: Shortcuts, options: MinderOperati
s: 'executeToSuccess', // 执行结果为成功 s: 'executeToSuccess', // 执行结果为成功
b: 'executeToBlocked', // 执行结果为阻塞 b: 'executeToBlocked', // 执行结果为阻塞
e: 'executeToError', // 执行结果为失败 e: 'executeToError', // 执行结果为失败
m: 'addSiblingModule', // 添加同级模块
c: 'addSiblingCase', // 添加同级用例
};
const shiftCombinationShortcuts: { [key: string]: ShortcutKey } = {
m: 'addChildModule', // 添加子级模块
c: 'addChildCase', // 添加子级用例
}; };
let action; let action;
if (isCtrlOrCmd && combinationShortcuts[key]) { if (isCtrlOrCmd && combinationShortcuts[key]) {
// 执行组合键事件 // 执行组合键事件
action = combinationShortcuts[key]; action = combinationShortcuts[key];
} else if (isShift && shiftCombinationShortcuts[key]) {
// 执行 shift 组合键事件
action = shiftCombinationShortcuts[key];
} else if (singleShortcuts[key]) { } else if (singleShortcuts[key]) {
// 执行单键事件 // 执行单键事件
action = singleShortcuts[key]; action = singleShortcuts[key];

View File

@ -43,14 +43,22 @@
<MsIcon type="icon-icon_full_screen_one" class="text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_full_screen_one" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<a-button v-if="!props.disabled" type="outline" class="px-[8px] py-[2px] text-[12px]" size="small" @click="save"> <a-button
v-if="!props.disabled"
type="outline"
class="flex items-center gap-[2px] px-[8px] py-[2px] text-[12px]"
size="small"
@click="save"
>
{{ t('minder.main.main.save') }} {{ t('minder.main.main.save') }}
<div>(<MsCtrlOrCommand :size="12" /> + S)</div>
</a-button> </a-button>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCtrlOrCommand from '@/components/pure/ms-ctrl-or-command';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import useFullScreen from '@/hooks/useFullScreen'; import useFullScreen from '@/hooks/useFullScreen';

View File

@ -6,7 +6,11 @@
:disabled="props.disabled" :disabled="props.disabled"
@save="save" @save="save"
/> />
<Navigator /> <Navigator :shortcut-list="props.shortcutList">
<template #shortCutList>
<slot name="shortCutList"></slot>
</template>
</Navigator>
<div <div
v-if="currentTreePath?.length > 0" v-if="currentTreePath?.length > 0"
class="absolute left-[50%] top-[16px] z-[9] w-[60%] translate-x-[-50%] overflow-hidden bg-white p-[8px]" class="absolute left-[50%] top-[16px] z-[9] w-[60%] translate-x-[-50%] overflow-hidden bg-white p-[8px]"
@ -64,6 +68,7 @@
MinderJson, MinderJson,
MinderJsonNode, MinderJsonNode,
MinderJsonNodeData, MinderJsonNodeData,
navigatorProps,
priorityProps, priorityProps,
tagProps, tagProps,
} from '../props'; } from '../props';
@ -80,6 +85,7 @@
...priorityProps, ...priorityProps,
...batchMenuProps, ...batchMenuProps,
...dropdownMenuProps, ...dropdownMenuProps,
...navigatorProps,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'save', data: MinderJson, callback: () => void): void; (e: 'save', data: MinderJson, callback: () => void): void;
@ -341,7 +347,7 @@
right: 7px; right: 7px;
font-size: 12px; font-size: 12px;
font-family: iconfont; font-family: iconfont;
content: '\e6d5'; content: '\e6fe';
color: var(--color-text-brand); color: var(--color-text-brand);
line-height: 16px; line-height: 16px;
transform: translateY(-50%); transform: translateY(-50%);

View File

@ -40,8 +40,8 @@
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<a-trigger <a-trigger
:popup-translate="[5, -105]" :popup-translate="[5, -20]"
position="right" position="rb"
class="ms-minder-shortcut-trigger" class="ms-minder-shortcut-trigger"
@popup-visible-change="(val) => (shortcutTriggerVisible = val)" @popup-visible-change="(val) => (shortcutTriggerVisible = val)"
> >
@ -60,7 +60,7 @@
<div>{{ t('minder.expand') }}</div> <div>{{ t('minder.expand') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon">/</div> <div class="ms-minder-shortcut-trigger-listitem-icon">/</div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('copy')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.copy') }}</div> <div>{{ t('common.copy') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
@ -69,13 +69,13 @@
<div class="ms-minder-shortcut-trigger-listitem-icon">C</div> <div class="ms-minder-shortcut-trigger-listitem-icon">C</div>
</div> </div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('addSibling')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.insetBrother') }}</div> <div>{{ t('minder.hotboxMenu.insetBrother') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
<MsIcon type="icon-icon_carriage_return2" /> <MsIcon type="icon-icon_carriage_return2" />
</div> </div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('paste')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.paste') }}</div> <div>{{ t('minder.hotboxMenu.paste') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
@ -84,13 +84,13 @@
<div class="ms-minder-shortcut-trigger-listitem-icon">V</div> <div class="ms-minder-shortcut-trigger-listitem-icon">V</div>
</div> </div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('addChild')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.insetSon') }}</div> <div>{{ t('minder.hotboxMenu.insetSon') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto"> <div class="ms-minder-shortcut-trigger-listitem-icon ms-minder-shortcut-trigger-listitem-icon-auto">
Tab Tab
</div> </div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('cut')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.cut') }}</div> <div>{{ t('minder.hotboxMenu.cut') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
@ -99,7 +99,7 @@
<div class="ms-minder-shortcut-trigger-listitem-icon">X</div> <div class="ms-minder-shortcut-trigger-listitem-icon">X</div>
</div> </div>
</div> </div>
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('enter')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('minder.hotboxMenu.enterNode') }}</div> <div>{{ t('minder.hotboxMenu.enterNode') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
@ -119,7 +119,7 @@
<div class="ms-minder-shortcut-trigger-listitem-icon">Z</div> <div class="ms-minder-shortcut-trigger-listitem-icon">Z</div>
</div> </div>
</div> --> </div> -->
<div class="ms-minder-shortcut-trigger-listitem"> <div v-if="props.shortcutList.includes('delete')" class="ms-minder-shortcut-trigger-listitem">
<div>{{ t('common.delete') }}</div> <div>{{ t('common.delete') }}</div>
<div class="ms-minder-shortcut-trigger-listitem-icon"> <div class="ms-minder-shortcut-trigger-listitem-icon">
<MsIcon type="icon-icon_carriage_return1" /> <MsIcon type="icon-icon_carriage_return1" />
@ -134,6 +134,7 @@
<div class="ms-minder-shortcut-trigger-listitem-icon">Y</div> <div class="ms-minder-shortcut-trigger-listitem-icon">Y</div>
</div> </div>
</div> --> </div> -->
<slot name="shortCutList"></slot>
</div> </div>
</template> </template>
</a-trigger> </a-trigger>
@ -149,14 +150,17 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { navigatorProps } from '../props';
import { getLocalStorage, setLocalStorage } from '../script/store'; import { getLocalStorage, setLocalStorage } from '../script/store';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
const props = defineProps(navigatorProps);
const { t } = useI18n(); const { t } = useI18n();
const navPreviewer: Ref<HTMLDivElement | null> = ref(null); const navPreviewer: Ref<HTMLDivElement | null> = ref(null);
const isNavOpen = ref(true); const isNavOpen = ref(false);
const previewNavigator: Ref<HTMLDivElement | null> = ref(null); const previewNavigator: Ref<HTMLDivElement | null> = ref(null);
const contentView = ref(''); const contentView = ref('');

View File

@ -14,6 +14,9 @@
<template #batchMenu> <template #batchMenu>
<slot name="batchMenu"></slot> <slot name="batchMenu"></slot>
</template> </template>
<template #shortCutList>
<slot name="shortCutList"></slot>
</template>
</mainEditor> </mainEditor>
</div> </div>
<div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']"> <div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']">
@ -51,6 +54,7 @@
MinderJson, MinderJson,
MinderJsonNode, MinderJsonNode,
moleProps, moleProps,
navigatorProps,
priorityProps, priorityProps,
tagProps, tagProps,
viewMenuProps, viewMenuProps,
@ -82,6 +86,7 @@
...viewMenuProps, ...viewMenuProps,
...batchMenuProps, ...batchMenuProps,
...dropdownMenuProps, ...dropdownMenuProps,
...navigatorProps,
}); });
const minderStore = useMinderStore(); const minderStore = useMinderStore();
@ -138,6 +143,9 @@
minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, undefined, [selectedNodes[0]]); minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, undefined, [selectedNodes[0]]);
} }
}, },
save: () => {
minderStore.dispatchEvent(MinderEventName.SAVE_MINDER);
},
delete: () => { delete: () => {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes(); const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
minderDelete(selectedNodes); minderDelete(selectedNodes);

View File

@ -328,3 +328,13 @@ export const viewMenuProps = {
default: true, default: true,
}, },
}; };
export const navigatorProps = {
// 显示的快捷键列表
shortcutList: {
type: Array as PropType<string[]>,
default() {
return ['expand', 'addSibling', 'addChild', 'delete', 'cut', 'copy', 'paste', 'enter'];
},
},
};

View File

@ -1,6 +1,5 @@
import '@7polo/kity/dist/kity'; import '@7polo/kity/dist/kity';
import '@7polo/kityminder-core'; import '@7polo/kityminder-core';
import clipboard from './runtime/clipboard';
import clipboardMimetype from './runtime/clipboard-mimetype'; import clipboardMimetype from './runtime/clipboard-mimetype';
import container from './runtime/container'; import container from './runtime/container';
import drag from './runtime/drag'; import drag from './runtime/drag';
@ -70,7 +69,6 @@ assemble(minder);
assemble(receiver); assemble(receiver);
assemble(input); assemble(input);
assemble(clipboardMimetype); assemble(clipboardMimetype);
assemble(clipboard);
assemble(drag); assemble(drag);
assemble(history); assemble(history);
assemble(jumping); assemble(jumping);

View File

@ -1,196 +0,0 @@
import { markDeleteNode, resetNodes } from '../tool/utils';
interface INode {
getLevel(): number;
isAncestorOf(node: INode): boolean;
appendChild(node: INode): INode;
}
interface IData {
getRegisterProtocol(protocol: string): {
encode: (nodes: Array<INode>) => Array<INode>;
decode: (nodes: Array<INode>) => Array<INode>;
};
}
export default function ClipboardRuntime(this: any) {
const { minder } = this;
const { receiver } = this;
const Data: IData = window.kityminder.data;
if (!minder.supportClipboardEvent || window.kity.Browser.gecko) {
return;
}
const kmencode = this.MimeType.getMimeTypeProtocol('application/km');
const { decode } = Data.getRegisterProtocol('json');
let _selectedNodes: Array<INode> = [];
function encode(nodes: Array<INode>): string {
const _nodes = [];
for (let i = 0, l = nodes.length; i < l; i++) {
// @ts-ignore
_nodes.push(minder.exportNode(nodes[i]));
}
return kmencode(Data.getRegisterProtocol('json').encode(_nodes));
}
const beforeCopy = (e: ClipboardEvent) => {
if (document.activeElement === receiver.element) {
const clipBoardEvent = e;
const state = this.fsm.state();
switch (state) {
case 'input': {
break;
}
case 'normal': {
const nodes = [...minder.getSelectedNodes()];
if (nodes.length) {
if (nodes.length > 1) {
let targetLevel;
nodes.sort((a: any, b: any) => {
return a.getLevel() - b.getLevel();
});
// eslint-disable-next-line prefer-const
targetLevel = nodes[0].getLevel();
if (targetLevel !== nodes[nodes.length - 1].getLevel()) {
let pnode;
let idx = 0;
const l = nodes.length;
let pidx = l - 1;
pnode = nodes[pidx];
while (pnode.getLevel() !== targetLevel) {
idx = 0;
while (idx < l && nodes[idx].getLevel() === targetLevel) {
if (nodes[idx].isAncestorOf(pnode)) {
nodes.splice(pidx, 1);
break;
}
idx++;
}
pidx--;
pnode = nodes[pidx];
}
}
}
const str = encode(nodes);
clipBoardEvent.clipboardData?.setData('text/plain', str);
}
e.preventDefault();
break;
}
default:
}
}
};
const beforeCut = (e: ClipboardEvent) => {
const { activeElement } = document;
if (activeElement === receiver.element) {
if (minder.getStatus() !== 'normal') {
e.preventDefault();
return;
}
const clipBoardEvent = e;
const state = this.fsm.state();
switch (state) {
case 'input': {
break;
}
case 'normal': {
markDeleteNode(minder);
const nodes = minder.getSelectedNodes();
if (nodes.length) {
clipBoardEvent.clipboardData?.setData('text/plain', encode(nodes));
minder.execCommand('removenode');
}
e.preventDefault();
break;
}
default:
}
}
};
const beforePaste = (e: ClipboardEvent) => {
if (document.activeElement === receiver.element) {
if (minder.getStatus() !== 'normal') {
e.preventDefault();
return;
}
const clipBoardEvent = e;
const state = this.fsm.state();
const textData = clipBoardEvent.clipboardData?.getData('text/plain');
switch (state) {
case 'input': {
// input状态下如果格式为application/km则不进行paste操作
if (!this.MimeType.isPureText(textData)) {
e.preventDefault();
return;
}
break;
}
case 'normal': {
/*
* normal状态下通过对选中节点粘贴导入子节点文本进行单独处理
*/
const sNodes = minder.getSelectedNodes();
if (this.MimeType.whichMimeType(textData) === 'application/km') {
const nodes = decode(this.MimeType.getPureText(textData));
resetNodes(nodes);
let _node;
sNodes.forEach((node: INode) => {
// 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来
for (let i = nodes.length - 1; i >= 0; i--) {
_node = minder.createNode(null, node);
minder.importNode(_node, nodes[i]);
_selectedNodes.push(_node);
node.appendChild(_node);
}
});
minder.select(_selectedNodes, true);
_selectedNodes = [];
minder.refresh();
} else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) {
const imageFile = clipBoardEvent.clipboardData.items[0].getAsFile();
const serverService = window.angular.element(document.body).injector().get('server');
return serverService.uploadImage(imageFile).then((json: Record<string, any>) => {
const resp = json.data;
if (resp.errno === 0) {
minder.execCommand('image', resp.data.url);
}
});
} else {
sNodes.forEach((node: INode) => {
minder.Text2Children(node, textData);
});
}
e.preventDefault();
break;
}
default:
}
// 触发命令监听
minder.execCommand('paste');
}
};
/**
* editor的receiver统一处理全部事件clipboard事件
* @Editor: Naixor
* @Date: 2015.9.24
*/
// TODO: 未来需要支持自定义快捷键处理逻辑
// document.addEventListener('copy', (e) => beforeCopy(e));
// document.addEventListener('cut', (e) => beforeCut(e));
// document.addEventListener('paste', (e) => beforePaste(e));
}

View File

@ -132,6 +132,12 @@ class FSM {
function FSMRuntime(this: any) { function FSMRuntime(this: any) {
this.fsm = new FSM('normal'); this.fsm = new FSM('normal');
this.fsm.when('normal -> normal', (exit: any, enter: any, reason: string, e: KeyboardEvent) => {
const arrowKey = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'];
if (reason === 'shortcut-handle' && arrowKey.includes(e.code)) {
this.minder.dispatchKeyEvent(e); // 触发脑图本身的方向快捷键事件
}
});
} }
export default FSMRuntime; export default FSMRuntime;

View File

@ -275,7 +275,7 @@ function InputRuntime(this: any) {
} }
} }
text = text.replace(/^\n*|\n*$/g, ''); text = text.replace(/^\n*|\n*$/g, '').replace(/<\/?p\b[^>]*>/gi, ''); // 去除富文本内p标签
text = text.replace(new RegExp(`(\n|\r|\n\r)(\u0020|${String.fromCharCode(160)}){4}`, 'g'), '$1\t'); text = text.replace(new RegExp(`(\n|\r|\n\r)(\u0020|${String.fromCharCode(160)}){4}`, 'g'), '$1\t');
this.minder.getSelectedNode().setText(text); this.minder.getSelectedNode().setText(text);
if (isBold) { if (isBold) {

View File

@ -32,6 +32,30 @@ interface IJumpingRuntime {
function JumpingRuntime(this: IJumpingRuntime): void { function JumpingRuntime(this: IJumpingRuntime): void {
const { fsm, receiver } = this; const { fsm, receiver } = this;
// normal -> *
receiver.listen('normal', (e: any) => {
// 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
receiver.enable();
/**
* check
* @editor Naixor
* @Date 2015-12-2
*/
switch (e.type) {
case 'keydown': {
// normal -> normal shortcut
fsm.jump('normal', 'shortcut-handle', e); // 触发快捷键事件,这里可能会被脑图自定义快捷键拦截处理,若非自定义快捷键则触发脑图本身的快捷键
break;
}
case 'keyup': {
break;
}
default: {
break;
}
}
});
// input => normal // input => normal
receiver.listen('input', (e: KeyboardEvent) => { receiver.listen('input', (e: KeyboardEvent) => {
receiver.enable(); receiver.enable();

View File

@ -209,6 +209,7 @@ export function createNode(data?: MinderJsonNodeData, parentNode?: MinderJsonNod
return window.minder.createNode( return window.minder.createNode(
{ {
...data, ...data,
text: data?.text.replace(/<\/?p\b[^>]*>/gi, '') || '',
expandState: 'collapse', expandState: 'collapse',
disabled: true, disabled: true,
}, },

View File

@ -87,6 +87,8 @@
class="w-[130px]" class="w-[130px]"
:disabled="fileSizeLimitLoading || !hasAnyPermission(['SYSTEM_PARAMETER_SETTING_BASE:READ+UPDATE'])" :disabled="fileSizeLimitLoading || !hasAnyPermission(['SYSTEM_PARAMETER_SETTING_BASE:READ+UPDATE'])"
:min="0" :min="0"
:max="1024"
:precision="0"
mode="button" mode="button"
@blur="() => saveFileSizeLimitConfig()" @blur="() => saveFileSizeLimitConfig()"
/> />