diff --git a/packages/editor/src/components/Tree.vue b/packages/editor/src/components/Tree.vue new file mode 100644 index 00000000..740fb70e --- /dev/null +++ b/packages/editor/src/components/Tree.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/editor/src/components/TreeNode.vue b/packages/editor/src/components/TreeNode.vue new file mode 100644 index 00000000..aeadddd1 --- /dev/null +++ b/packages/editor/src/components/TreeNode.vue @@ -0,0 +1,142 @@ + + + diff --git a/packages/editor/src/layouts/sidebar/layer/LayerNode.vue b/packages/editor/src/layouts/sidebar/layer/LayerNode.vue deleted file mode 100644 index 23a275c6..00000000 --- a/packages/editor/src/layouts/sidebar/layer/LayerNode.vue +++ /dev/null @@ -1,208 +0,0 @@ - - - diff --git a/packages/editor/src/layouts/sidebar/layer/LayerPanel.vue b/packages/editor/src/layouts/sidebar/layer/LayerPanel.vue index a51bdc6f..f4ff892e 100644 --- a/packages/editor/src/layouts/sidebar/layer/LayerPanel.vue +++ b/packages/editor/src/layouts/sidebar/layer/LayerPanel.vue @@ -1,22 +1,31 @@ diff --git a/packages/editor/src/layouts/sidebar/layer/use-click.ts b/packages/editor/src/layouts/sidebar/layer/use-click.ts new file mode 100644 index 00000000..b0d028cd --- /dev/null +++ b/packages/editor/src/layouts/sidebar/layer/use-click.ts @@ -0,0 +1,105 @@ +import { type ComputedRef, nextTick, type Ref, ref } from 'vue'; +import { throttle } from 'lodash-es'; + +import { Id, MNode } from '@tmagic/schema'; + +import { LayerNodeStatus, Services, TreeNodeData, UI_SELECT_MODE_EVENT_NAME } from '@editor/type'; +import { updateStatus } from '@editor/utils/tree'; + +import LayerMenu from './LayerMenu.vue'; + +export const useClick = ( + services: Services | undefined, + isCtrlKeyDown: Ref, + nodeStatusMap: ComputedRef | undefined>, +) => { + // 触发画布选中 + const select = async (data: MNode) => { + if (!data.id) { + throw new Error('没有id'); + } + + if (isCtrlKeyDown.value) { + multiSelect(data); + } else { + await services?.editorService.select(data); + services?.editorService.get('stage')?.select(data.id); + } + }; + + const multiSelect = async (data: MNode) => { + const nodes = services?.editorService.get('nodes') || []; + + const newNodes: Id[] = []; + let isCancel = false; + nodes.forEach((node) => { + if (node.id === data.id) { + isCancel = true; + return; + } + + newNodes.push(node.id); + }); + + // 只剩一个不能取消选中 + if (!isCancel || newNodes.length === 0) { + newNodes.push(data.id); + } + + await services?.editorService.multiSelect(newNodes); + services?.editorService.get('stage')?.multiSelect(newNodes); + }; + + const throttleTime = 300; + // 鼠标在组件树移动触发高亮 + const highlightHandler = throttle((event: MouseEvent, data: TreeNodeData) => { + highlight(data); + }, throttleTime); + + // 触发画布高亮 + const highlight = (data: TreeNodeData) => { + services?.editorService?.highlight(data); + services?.editorService?.get('stage')?.highlight(data.id); + }; + + const nodeClickHandler = (event: MouseEvent, data: TreeNodeData) => { + if (!nodeStatusMap?.value) return; + + if (services?.uiService.get('uiSelectMode')) { + document.dispatchEvent(new CustomEvent(UI_SELECT_MODE_EVENT_NAME, { detail: data })); + return; + } + + if (data.items && data.items.length > 0 && !isCtrlKeyDown.value) { + updateStatus(nodeStatusMap.value, data.id, { + expand: true, + }); + } + + nextTick(() => { + select(data); + }); + }; + + // 右键菜单 + const menu = ref>(); + + return { + menu, + + nodeClickHandler, + + nodeContentmenuHandler(event: MouseEvent, data: TreeNodeData) { + event.preventDefault(); + + const nodes = services?.editorService.get('nodes') || []; + if (nodes.length < 2 || !nodes.includes(data)) { + nodeClickHandler(event, data); + } + + menu.value?.show(event); + }, + + highlightHandler, + }; +}; diff --git a/packages/editor/src/layouts/sidebar/layer/use-filter.ts b/packages/editor/src/layouts/sidebar/layer/use-filter.ts index 11dee902..9e856aef 100644 --- a/packages/editor/src/layouts/sidebar/layer/use-filter.ts +++ b/packages/editor/src/layouts/sidebar/layer/use-filter.ts @@ -1,21 +1,10 @@ import { type ComputedRef, ref } from 'vue'; import { Id, MNode, MPage } from '@tmagic/schema'; -import { getKeys } from '@tmagic/utils'; import { LayerNodeStatus } from '@editor/type'; import { traverseNode } from '@editor/utils'; - -export const updateStatus = (nodeStatusMap: Map, id: Id, status: Partial) => { - const nodeStatus = nodeStatusMap.get(id); - - if (!nodeStatus) return; - getKeys(status).forEach((key) => { - if (nodeStatus[key] !== undefined && status[key] !== undefined) { - nodeStatus[key] = Boolean(status[key]); - } - }); -}; +import { updateStatus } from '@editor/utils/tree'; export const useFilter = ( nodeStatusMap: ComputedRef | undefined>, diff --git a/packages/editor/src/layouts/sidebar/layer/use-keybinding.ts b/packages/editor/src/layouts/sidebar/layer/use-keybinding.ts index 98e299e3..ee6ca044 100644 --- a/packages/editor/src/layouts/sidebar/layer/use-keybinding.ts +++ b/packages/editor/src/layouts/sidebar/layer/use-keybinding.ts @@ -1,9 +1,13 @@ import { type Ref, ref, watchEffect } from 'vue'; +import Tree from '@editor/components/Tree.vue'; import type { Services } from '@editor/type'; import { KeyBindingContainerKey } from '@editor/utils/keybinding-config'; -export const useKeybinding = (services: Services | undefined, contianer: Ref) => { +export const useKeybinding = ( + services: Services | undefined, + contianer: Ref | undefined>, +) => { const keybindingService = services?.keybindingService; // 是否多选 @@ -37,7 +41,7 @@ export const useKeybinding = (services: Services | undefined, contianer: Ref { if (contianer.value) { globalThis.addEventListener('blur', windowBlurHandler); - keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value); + keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value.$el); } else { globalThis.removeEventListener('blur', windowBlurHandler); keybindingService?.unregisteEl(KeyBindingContainerKey.LAYER_PANEL); diff --git a/packages/editor/src/layouts/sidebar/layer/use-node-status.ts b/packages/editor/src/layouts/sidebar/layer/use-node-status.ts index 6698a0b2..95ca5c72 100644 --- a/packages/editor/src/layouts/sidebar/layer/use-node-status.ts +++ b/packages/editor/src/layouts/sidebar/layer/use-node-status.ts @@ -5,8 +5,7 @@ import { getNodePath } from '@tmagic/utils'; import { LayerNodeStatus, Services } from '@editor/type'; import { traverseNode } from '@editor/utils'; - -import { updateStatus } from './use-filter'; +import { updateStatus } from '@editor/utils/tree'; const createPageNodeStatus = (services: Services | undefined, pageId: Id) => { const map = new Map(); @@ -15,6 +14,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => { visible: true, expand: true, selected: true, + draggable: false, }); services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) => @@ -23,6 +23,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => { visible: true, expand: false, selected: false, + draggable: true, }); }), ); @@ -87,6 +88,7 @@ export const useNodeStatus = (services: Services | undefined, page: ComputedRef< visible: true, expand: Array.isArray(node.items), selected: true, + draggable: true, }); }); }); diff --git a/packages/editor/src/theme/layer-panel.scss b/packages/editor/src/theme/layer-panel.scss index c7e93942..70433a09 100644 --- a/packages/editor/src/theme/layer-panel.scss +++ b/packages/editor/src/theme/layer-panel.scss @@ -1,79 +1,7 @@ -.magic-editor-layer-panel { - $--node-height: 22px; - +.m-editor-layer-panel { background: $--sidebar-content-background-color; - .magic-editor-layer-tree { + .m-editor-tree { padding-top: 48px; - background-color: $--sidebar-content-background-color; - color: $--font-color; - font-size: 13px; - - .magic-editor-layer-node { - cursor: pointer; - white-space: nowrap; - - .layer-node { - display: flex; - align-items: center; - - &:hover { - background-color: $--hover-color; - color: $--font-color; - } - - &.selected { - background-color: $--theme-color; - color: $--hover-color; - } - - &.drag-inner { - .layer-node-content { - background-color: rgba($color: $--theme-color, $alpha: 0.5); - color: $--hover-color; - } - } - - &.drag-before { - .layer-node-content { - border-top-color: rgba($color: $--theme-color, $alpha: 0.5); - } - } - - &.drag-after { - .layer-node-content { - border-bottom-color: rgba($color: $--theme-color, $alpha: 0.5); - } - } - - .expand-icon { - padding: 4px; - box-sizing: content-box; - font-size: 14px; - } - - .layer-node-content { - display: flex; - flex: 1; - justify-content: space-between; - height: $--node-height; - border-top: 2px solid transparent; - border-bottom: 2px solid transparent; - .layer-node-label { - line-height: $--node-height; - flex: 1; - width: 100px; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .layer-node-tool { - margin-right: 15px; - display: flex; - align-items: center; - } - } - } } } diff --git a/packages/editor/src/theme/theme.scss b/packages/editor/src/theme/theme.scss index 048650fe..5917c4f7 100644 --- a/packages/editor/src/theme/theme.scss +++ b/packages/editor/src/theme/theme.scss @@ -22,3 +22,4 @@ @import "./data-source-methods.scss"; @import "./data-source-input.scss"; @import "./key-value.scss"; +@import "./tree.scss"; diff --git a/packages/editor/src/theme/tree.scss b/packages/editor/src/theme/tree.scss new file mode 100644 index 00000000..24d5ca85 --- /dev/null +++ b/packages/editor/src/theme/tree.scss @@ -0,0 +1,72 @@ +.m-editor-tree { + $--node-height: 22px; + color: $--font-color; + font-size: 13px; + + .m-editor-tree-node { + cursor: pointer; + white-space: nowrap; + + .tree-node { + display: flex; + align-items: center; + + &:hover { + background-color: $--hover-color; + color: $--font-color; + } + + &.selected { + background-color: $--theme-color; + color: $--hover-color; + } + + &.drag-inner { + .tree-node-content { + background-color: rgba($color: $--theme-color, $alpha: 0.5); + color: $--hover-color; + } + } + + &.drag-before { + .tree-node-content { + border-top-color: rgba($color: $--theme-color, $alpha: 0.5); + } + } + + &.drag-after { + .tree-node-content { + border-bottom-color: rgba($color: $--theme-color, $alpha: 0.5); + } + } + + .expand-icon { + padding: 4px; + box-sizing: content-box; + font-size: 14px; + } + + .tree-node-content { + display: flex; + flex: 1; + justify-content: space-between; + height: $--node-height; + border-top: 2px solid transparent; + border-bottom: 2px solid transparent; + .tree-node-label { + line-height: $--node-height; + flex: 1; + width: 100px; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .tree-node-tool { + margin-right: 15px; + display: flex; + align-items: center; + } + } + } +} diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index 0102aa7a..8120b069 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -595,6 +595,8 @@ export interface LayerNodeStatus { expand: boolean; /** 选中 */ selected: boolean; + /** 是否可拖拽 */ + draggable: boolean; } /** 拖拽类型 */ @@ -607,3 +609,9 @@ export enum DragType { /** 当uiService.get('uiSelectMode')为true,点击组件(包括任何形式,组件树/画布)时触发的事件名 */ export const UI_SELECT_MODE_EVENT_NAME = 'ui-select'; + +export interface TreeNodeData { + id: Id; + name?: string; + items?: TreeNodeData[]; +} diff --git a/packages/editor/src/utils/tree.ts b/packages/editor/src/utils/tree.ts new file mode 100644 index 00000000..6cf234c6 --- /dev/null +++ b/packages/editor/src/utils/tree.ts @@ -0,0 +1,15 @@ +import type { Id } from '@tmagic/schema'; +import { getKeys } from '@tmagic/utils'; + +import type { LayerNodeStatus } from '@editor/type'; + +export const updateStatus = (nodeStatusMap: Map, id: Id, status: Partial) => { + const nodeStatus = nodeStatusMap.get(id); + + if (!nodeStatus) return; + getKeys(status).forEach((key) => { + if (nodeStatus[key] !== undefined && status[key] !== undefined) { + nodeStatus[key] = Boolean(status[key]); + } + }); +};