mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-13 00:09:20 +08:00
refactor(editor): 优化组件树逻辑
This commit is contained in:
parent
87a19c9bae
commit
3934ebd030
@ -14,42 +14,33 @@
|
||||
|
||||
<TMagicTree
|
||||
v-if="values.length"
|
||||
tabindex="-1"
|
||||
class="magic-editor-layer-tree"
|
||||
ref="tree"
|
||||
node-key="id"
|
||||
empty-text="页面空荡荡的"
|
||||
draggable
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:load="loadItems"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:default-checked-keys="checkedKeys"
|
||||
:current-node-key="currentNodeKey"
|
||||
:data="values"
|
||||
:default-expand-all="true"
|
||||
:expand-on-click-node="false"
|
||||
:highlight-current="true"
|
||||
:props="{
|
||||
children: 'items',
|
||||
}"
|
||||
:highlight-current="!isMultiSelect"
|
||||
:check-on-click-node="true"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
:allow-drop="allowDrop"
|
||||
:show-checkbox="isMultiSelectStatus || selectedIds.length > 1"
|
||||
:show-checkbox="isMultiSelect"
|
||||
@node-click="clickHandler"
|
||||
@node-contextmenu="contextmenu"
|
||||
@node-drag-end="handleDragEnd"
|
||||
@node-collapse="handleCollapse"
|
||||
@node-expand="handleExpand"
|
||||
@check="multiClickHandler"
|
||||
@check="checkHandler"
|
||||
@mousedown="toggleClickFlag"
|
||||
@mouseup="toggleClickFlag"
|
||||
@mouseenter="mouseenterHandler"
|
||||
@mouseleave="mouseleaveHandler"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div
|
||||
:id="data.id"
|
||||
class="cus-tree-node"
|
||||
@mouseenter="highlightHandler(data)"
|
||||
:class="{ 'cus-tree-node-hover': canHighlight(data) }"
|
||||
>
|
||||
<div class="cus-tree-node" :id="data.id" @mouseenter="highlightHandler(data)">
|
||||
<slot name="layer-node-content" :node="node" :data="data">
|
||||
<span>
|
||||
{{ `${data.name} (${data.id})` }}
|
||||
@ -66,15 +57,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="MEditorLayerPanel">
|
||||
import { computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import KeyController from 'keycon';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { difference, throttle, union } from 'lodash-es';
|
||||
|
||||
import { TMagicInput, TMagicScrollbar, TMagicTree } from '@tmagic/design';
|
||||
import type { Id, MNode, MPage } from '@tmagic/schema';
|
||||
import { MContainer, NodeType } from '@tmagic/schema';
|
||||
import StageCore from '@tmagic/stage';
|
||||
import { getNodePath, isPage } from '@tmagic/utils';
|
||||
|
||||
import type { MenuButton, MenuComponent, Services } from '../../type';
|
||||
import { Layout } from '../../type';
|
||||
@ -87,22 +79,40 @@ defineProps<{
|
||||
|
||||
const throttleTime = 150;
|
||||
const services = inject<Services>('services');
|
||||
const editorService = services?.editorService;
|
||||
|
||||
const tree = ref<InstanceType<typeof TMagicTree>>();
|
||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||
const editorService = services?.editorService;
|
||||
const page = computed(() => editorService?.get('page'));
|
||||
const values = computed(() => (page.value ? [page.value] : []));
|
||||
// 多选选中的节点数组
|
||||
const selectedNodes = ref<MNode[]>([]);
|
||||
|
||||
// 多选选中的组件id数组
|
||||
const selectedIds = computed(() => selectedNodes.value.map((node: MNode) => node.id));
|
||||
const checkedKeys = ref<Id[]>([]);
|
||||
// 是否多选
|
||||
const isMultiSelectStatus = ref(false);
|
||||
// 多选场景 取消选中的那个节点id
|
||||
const spliceNodeKey = ref<Id>();
|
||||
const isCtrlKeyDown = ref(false);
|
||||
const filterText = ref('');
|
||||
// 默认展开节点
|
||||
const defaultExpandedKeys = computed(() => (selectedIds.value.length > 0 ? selectedIds.value : []));
|
||||
const expandedKeys = ref<Id[]>([]);
|
||||
const currentNodeKey = ref<Id>();
|
||||
// 鼠标是否按下标志,用于高亮状态互斥
|
||||
const clicked = ref(false);
|
||||
|
||||
const treeProps = {
|
||||
children: 'items',
|
||||
disabled: (data: MNode) => Boolean(data.items?.length),
|
||||
class: (data: MNode) => {
|
||||
if (clicked.value || isPage(data)) return '';
|
||||
if (data.id === highlightNode?.value?.id && !checkedKeys.value.includes(data.id)) {
|
||||
return 'cus-tree-node-hover';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const isMultiSelect = computed(() => isCtrlKeyDown.value || checkedKeys.value.length > 1);
|
||||
|
||||
const nodes = computed(() => editorService?.get<MNode[]>('nodes') || []);
|
||||
const page = computed(() => editorService?.get<MPage>('page'));
|
||||
const values = computed(() => (page.value ? [page.value] : []));
|
||||
// 高亮的节点
|
||||
const highlightNode = computed(() => editorService?.get('highlightNode'));
|
||||
|
||||
// 触发画布单选
|
||||
const select = async (data: MNode) => {
|
||||
@ -129,8 +139,6 @@ const highlight = (data: MNode) => {
|
||||
editorService?.get<StageCore>('stage')?.highlight(data.id);
|
||||
};
|
||||
|
||||
const expandedKeys = new Map<Id, Id>();
|
||||
|
||||
// tree方法:拖拽时判定目标节点能否成为拖动目标位置
|
||||
const allowDrop = (draggingNode: any, dropNode: any, type: string): boolean => {
|
||||
const { data } = dropNode || {};
|
||||
@ -163,27 +171,22 @@ const handleDragEnd = async (e: any) => {
|
||||
editorService?.update(page);
|
||||
};
|
||||
|
||||
// tree方法: 加载子树数据的方法
|
||||
const loadItems = (node: any, resolve: Function) => {
|
||||
if (Array.isArray(node.data)) {
|
||||
return resolve(node.data);
|
||||
}
|
||||
if (Array.isArray(node.data?.items)) {
|
||||
return resolve(node.data?.items);
|
||||
}
|
||||
resolve([]);
|
||||
};
|
||||
|
||||
// tree事件:节点被关闭时触发的事件
|
||||
const handleCollapse = (data: MNode) => {
|
||||
expandedKeys.delete(data.id);
|
||||
expandedKeys.value = expandedKeys.value.filter((id) => id !== data.id);
|
||||
};
|
||||
|
||||
// tree事件:节点被展开时触发的事件
|
||||
const handleExpand = (data: MNode) => {
|
||||
const parent = editorService?.getParentById(data.id);
|
||||
if (!parent?.id) return;
|
||||
expandedKeys.set(parent.id, parent.id);
|
||||
if (!page.value) {
|
||||
expandedKeys.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
expandedKeys.value = union(
|
||||
expandedKeys.value,
|
||||
getNodePath(data.id, [page.value]).map((node) => node.id),
|
||||
);
|
||||
};
|
||||
|
||||
// tree方法:对树节点进行筛选时执行的方法
|
||||
@ -205,67 +208,32 @@ const filterTextChangeHandler = (val: string) => {
|
||||
tree.value?.filter(val);
|
||||
};
|
||||
|
||||
// 树节点更新后展开上次展开过的节点
|
||||
const expandNodes = async () => {
|
||||
if (!tree.value) return;
|
||||
await nextTick();
|
||||
tree.value &&
|
||||
Object.entries(tree.value.getStore().nodesMap).forEach(([id, node]: [string, any]) => {
|
||||
if (node.expanded && node.data.items) {
|
||||
expandedKeys.set(id, id);
|
||||
}
|
||||
});
|
||||
expandedKeys.forEach((key) => {
|
||||
if (!tree.value) return;
|
||||
tree.value.getNode(key)?.expand();
|
||||
});
|
||||
};
|
||||
watch(nodes, (nodes) => {
|
||||
const ids = nodes?.map((node) => node.id) || [];
|
||||
|
||||
watch(
|
||||
() => editorService?.get<MNode[]>('nodes'),
|
||||
(nodes) => {
|
||||
selectedNodes.value = nodes ?? [];
|
||||
},
|
||||
);
|
||||
const idsLength = ids.length;
|
||||
const checkedKeysLength = checkedKeys.value.length;
|
||||
|
||||
// 设置树节点选中状态
|
||||
const setTreeKeyStatus = () => {
|
||||
if (!tree.value) return;
|
||||
if (selectedIds.value.length === 0) {
|
||||
tree.value.setCheckedKeys([]);
|
||||
tree.value.setCurrentKey();
|
||||
} else if (selectedIds.value.length === 1 && !isMultiSelectStatus.value) {
|
||||
// 选中1个
|
||||
tree.value.setCurrentKey(selectedIds.value[0], true);
|
||||
tree.value.setCheckedKeys([]);
|
||||
} else {
|
||||
// 多选框选中多个
|
||||
tree.value.setCheckedKeys(selectedIds.value);
|
||||
tree.value.setCurrentKey();
|
||||
if (
|
||||
difference(
|
||||
idsLength > checkedKeysLength ? ids : checkedKeys.value,
|
||||
idsLength > checkedKeysLength ? checkedKeys.value : ids,
|
||||
).length
|
||||
) {
|
||||
tree.value?.setCheckedKeys([], false);
|
||||
|
||||
[currentNodeKey.value] = ids;
|
||||
checkedKeys.value = ids.filter((id) => id !== page.value?.id);
|
||||
expandedKeys.value = union(expandedKeys.value, ids);
|
||||
}
|
||||
};
|
||||
|
||||
watch([selectedIds, tree], async () => {
|
||||
if (!tree.value || !editorService) return;
|
||||
const parent = editorService.get('parent');
|
||||
if (!parent?.id) return;
|
||||
|
||||
const treeNode = tree.value.getNode(parent.id);
|
||||
treeNode?.updateChildren();
|
||||
await expandNodes();
|
||||
|
||||
// 设置高亮节点操作一定要在刷新展开状态之后,否则可能导致设置的高亮无效
|
||||
setTreeKeyStatus();
|
||||
});
|
||||
|
||||
const mouseenterHandler = () => {
|
||||
tree.value?.$el.focus();
|
||||
};
|
||||
|
||||
const mouseleaveHandler = () => {
|
||||
tree.value?.$el.blur();
|
||||
isMultiSelectStatus.value = false;
|
||||
};
|
||||
watch(isMultiSelect, (isMultiSelect) => {
|
||||
if (!isMultiSelect) {
|
||||
currentNodeKey.value = editorService?.get<MNode>('node').id;
|
||||
tree.value?.setCurrentKey(currentNodeKey.value);
|
||||
}
|
||||
});
|
||||
|
||||
const editorServiceRemoveHandler = () => {
|
||||
setTimeout(() => {
|
||||
@ -273,37 +241,42 @@ const editorServiceRemoveHandler = () => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const windowBlurHandler = () => {
|
||||
isCtrlKeyDown.value = false;
|
||||
};
|
||||
|
||||
let keycon: KeyController;
|
||||
|
||||
onMounted(() => {
|
||||
editorService?.on('remove', editorServiceRemoveHandler);
|
||||
|
||||
keycon = new KeyController(tree.value?.$el);
|
||||
keycon = new KeyController();
|
||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||
|
||||
keycon
|
||||
.keydown(ctrl, (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
isMultiSelectStatus.value = true;
|
||||
.keydown((e) => {
|
||||
if (e.key !== ctrl) {
|
||||
isCtrlKeyDown.value = false;
|
||||
}
|
||||
})
|
||||
.keyup(ctrl, (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
isMultiSelectStatus.value = false;
|
||||
.keydown(ctrl, () => {
|
||||
isCtrlKeyDown.value = true;
|
||||
})
|
||||
.keyup(ctrl, () => {
|
||||
isCtrlKeyDown.value = false;
|
||||
});
|
||||
|
||||
globalThis.addEventListener('blur', windowBlurHandler);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
keycon.destroy();
|
||||
|
||||
editorService?.off('remove', editorServiceRemoveHandler);
|
||||
globalThis.removeEventListener('blur', windowBlurHandler);
|
||||
});
|
||||
|
||||
// 鼠标是否按下标志,用于高亮状态互斥
|
||||
const clicked = ref(false);
|
||||
// 高亮的节点
|
||||
const highlightNode = computed(() => editorService?.get('highlightNode'));
|
||||
|
||||
// 鼠标在组件树移动触发高亮
|
||||
const highlightHandler = throttle((data: MNode) => {
|
||||
highlight(data);
|
||||
@ -313,58 +286,25 @@ const toggleClickFlag = () => {
|
||||
clicked.value = !clicked.value;
|
||||
};
|
||||
|
||||
// 是否满足展示高亮
|
||||
const canHighlight = (data: MNode) => {
|
||||
if (clicked.value) return false;
|
||||
return (
|
||||
data.id === highlightNode?.value?.id && !selectedIds.value.includes(data.id) && spliceNodeKey.value !== data.id
|
||||
);
|
||||
};
|
||||
|
||||
// 监听选择模式,针对多选情况做一些处理
|
||||
watch(isMultiSelectStatus, () => {
|
||||
// 多选模式如果已存在第一个选中的元素是页面(magic-ui-page) 剔除页面选中状态
|
||||
if (isMultiSelectStatus.value && selectedNodes.value.length === 1 && selectedNodes.value[0].type === NodeType.PAGE) {
|
||||
selectedNodes.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
// 选择节点多选框
|
||||
const multiClickHandler = (data: MNode): void => {
|
||||
if (!data?.id) {
|
||||
throw new Error('没有id');
|
||||
}
|
||||
|
||||
// 页面(magic-ui-page)不可选中
|
||||
if (data.type === NodeType.PAGE) {
|
||||
tree.value?.setCheckedKeys([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = selectedNodes.value.findIndex((node) => node.id === data.id);
|
||||
if (index !== -1) {
|
||||
// 已经包含就移除掉
|
||||
selectedNodes.value.splice(index, 1);
|
||||
spliceNodeKey.value = data.id;
|
||||
const checkHandler = (data: MNode, { checkedNodes }: any): void => {
|
||||
if (checkedNodes.length > 0) {
|
||||
multiSelect(checkedNodes.map((node: MNode) => node.id));
|
||||
} else {
|
||||
selectedNodes.value = [...selectedNodes.value, data];
|
||||
multiSelect(nodes.value.map((node: MNode) => node.id));
|
||||
}
|
||||
tree.value?.setCheckedKeys(selectedIds.value);
|
||||
multiSelect(selectedIds.value);
|
||||
};
|
||||
|
||||
// 点击节点
|
||||
const clickHandler = (data: MNode): void => {
|
||||
if (!isMultiSelectStatus.value) {
|
||||
if (services?.uiService.get<boolean>('uiSelectMode')) {
|
||||
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
|
||||
return;
|
||||
}
|
||||
tree.value?.setCurrentKey(data.id);
|
||||
select(data);
|
||||
} else {
|
||||
multiClickHandler(data);
|
||||
if (isCtrlKeyDown.value) {
|
||||
return;
|
||||
}
|
||||
if (services?.uiService.get<boolean>('uiSelectMode')) {
|
||||
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
|
||||
return;
|
||||
}
|
||||
select(data);
|
||||
};
|
||||
|
||||
// 右键菜单
|
||||
|
Loading…
x
Reference in New Issue
Block a user