mirror of
				https://github.com/Tencent/tmagic-editor.git
				synced 2025-11-04 18:52:18 +08:00 
			
		
		
		
	feat(editor): 新增右键菜单移动至其他页面功能
This commit is contained in:
		
							parent
							
								
									b0f2ed55f6
								
							
						
					
					
						commit
						434bf2ed70
					
				@ -4,10 +4,11 @@
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        v-for="(item, index) in menuData"
 | 
			
		||||
        event-type="mouseup"
 | 
			
		||||
        ref="buttons"
 | 
			
		||||
        :data="item"
 | 
			
		||||
        :key="index"
 | 
			
		||||
        @mouseup="hide"
 | 
			
		||||
        @mouseenter="showSubMenu(item)"
 | 
			
		||||
        @mouseenter="showSubMenu(item, index)"
 | 
			
		||||
      ></ToolButton>
 | 
			
		||||
    </div>
 | 
			
		||||
    <teleport to="body">
 | 
			
		||||
@ -48,6 +49,7 @@ const props = withDefaults(
 | 
			
		||||
const emit = defineEmits(['hide', 'show']);
 | 
			
		||||
 | 
			
		||||
const menu = ref<HTMLDivElement>();
 | 
			
		||||
const buttons = ref<InstanceType<typeof ToolButton>[]>();
 | 
			
		||||
const subMenu = ref<any>();
 | 
			
		||||
const visible = ref(false);
 | 
			
		||||
const subMenuData = ref<(MenuButton | MenuComponent)[]>([]);
 | 
			
		||||
@ -101,7 +103,7 @@ const show = (e: MouseEvent) => {
 | 
			
		||||
  }, 300);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showSubMenu = (item: MenuButton | MenuComponent) => {
 | 
			
		||||
const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
 | 
			
		||||
  const menuItem = item as MenuButton;
 | 
			
		||||
  if (typeof item !== 'object' || !menuItem.items?.length) {
 | 
			
		||||
    return;
 | 
			
		||||
@ -110,9 +112,15 @@ const showSubMenu = (item: MenuButton | MenuComponent) => {
 | 
			
		||||
  subMenuData.value = menuItem.items || [];
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    if (menu.value) {
 | 
			
		||||
      // 将子菜单放置在按钮右侧,与按钮齐平
 | 
			
		||||
      let y = menu.value.offsetTop;
 | 
			
		||||
      if (buttons.value?.[index].$el) {
 | 
			
		||||
        const rect = buttons.value?.[index].$el.getBoundingClientRect();
 | 
			
		||||
        y = rect.top;
 | 
			
		||||
      }
 | 
			
		||||
      subMenu.value?.show({
 | 
			
		||||
        clientX: menu.value.offsetLeft + menu.value.clientWidth,
 | 
			
		||||
        clientY: menu.value.offsetTop,
 | 
			
		||||
        clientY: y,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, 0);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								packages/editor/src/icons/CenterIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/editor/src/icons/CenterIcon.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path fill-rule="evenodd" clip-rule="evenodd" d="M2 4H21V6H2V4Z" fill="black" fill-opacity="0.9" />
 | 
			
		||||
    <path fill-rule="evenodd" clip-rule="evenodd" d="M5 11H18V13H5V11Z" fill="black" fill-opacity="0.9" />
 | 
			
		||||
    <path fill-rule="evenodd" clip-rule="evenodd" d="M2 18H21V20H2V18Z" fill="black" fill-opacity="0.9" />
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'MEditorCenterIcon',
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@ -4,12 +4,11 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, inject, markRaw, ref } from 'vue';
 | 
			
		||||
import { CopyDocument, Delete, Files, Plus } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import { NodeType } from '@tmagic/schema';
 | 
			
		||||
import { Files, Plus } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import type { ComponentGroup, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'MEditorLayerMenu',
 | 
			
		||||
@ -23,8 +22,6 @@ const services = inject<Services>('services');
 | 
			
		||||
const menu = ref<InstanceType<typeof ContentMenu>>();
 | 
			
		||||
const node = computed(() => services?.editorService.get('node'));
 | 
			
		||||
const nodes = computed(() => services?.editorService.get('nodes'));
 | 
			
		||||
const isRoot = computed(() => node.value?.type === NodeType.ROOT);
 | 
			
		||||
const isPage = computed(() => node.value?.type === NodeType.PAGE);
 | 
			
		||||
const componentList = computed(() => services?.componentListService.getList() || []);
 | 
			
		||||
 | 
			
		||||
const createMenuItems = (group: ComponentGroup): MenuButton[] =>
 | 
			
		||||
@ -86,24 +83,10 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
 | 
			
		||||
    display: () => node.value?.items && nodes.value?.length === 1,
 | 
			
		||||
    items: getSubMenuData.value,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '复制',
 | 
			
		||||
    icon: markRaw(CopyDocument),
 | 
			
		||||
    display: () => !isRoot.value,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      node.value && services?.editorService.copy(nodes.value || []);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '删除',
 | 
			
		||||
    icon: markRaw(Delete),
 | 
			
		||||
    display: () => !isRoot.value && !isPage.value,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      node.value && services?.editorService.remove(nodes.value || []);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  useCopyMenu(),
 | 
			
		||||
  usePasteMenu(),
 | 
			
		||||
  useDeleteMenu(),
 | 
			
		||||
  useMoveToMenu(services),
 | 
			
		||||
  ...props.layerContentMenu,
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,16 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, inject, markRaw, ref, watch } from 'vue';
 | 
			
		||||
import { Bottom, CopyDocument, Delete, DocumentCopy, Top } from '@element-plus/icons-vue';
 | 
			
		||||
import { Bottom, Top } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import { NodeType } from '@tmagic/schema';
 | 
			
		||||
import { isPage } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import CenterIcon from '@editor/icons/CenterIcon.vue';
 | 
			
		||||
import storageService from '@editor/services/storage';
 | 
			
		||||
import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
			
		||||
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
@ -32,42 +34,20 @@ const canCenter = ref(false);
 | 
			
		||||
const node = computed(() => editorService?.get('node'));
 | 
			
		||||
const nodes = computed(() => editorService?.get('nodes'));
 | 
			
		||||
const parent = computed(() => editorService?.get('parent'));
 | 
			
		||||
const stage = computed(() => editorService?.get('stage'));
 | 
			
		||||
 | 
			
		||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '水平居中',
 | 
			
		||||
    icon: markRaw(CenterIcon),
 | 
			
		||||
    display: () => canCenter.value,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      if (!nodes.value) return;
 | 
			
		||||
      editorService?.alignCenter(nodes.value);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '复制',
 | 
			
		||||
    icon: markRaw(CopyDocument),
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      nodes.value && editorService?.copy(nodes.value);
 | 
			
		||||
      canPaste.value = true;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '粘贴',
 | 
			
		||||
    icon: markRaw(DocumentCopy),
 | 
			
		||||
    display: () => canPaste.value,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      const rect = menu.value?.$el.getBoundingClientRect();
 | 
			
		||||
      const parentRect = stage.value?.container?.getBoundingClientRect();
 | 
			
		||||
      const initialLeft = (rect?.left || 0) - (parentRect?.left || 0);
 | 
			
		||||
      const initialTop = (rect?.top || 0) - (parentRect?.top || 0);
 | 
			
		||||
 | 
			
		||||
      if (!nodes.value || nodes.value.length === 0) return;
 | 
			
		||||
      editorService?.paste({ left: initialLeft, top: initialTop });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  useCopyMenu(),
 | 
			
		||||
  usePasteMenu(menu),
 | 
			
		||||
  {
 | 
			
		||||
    type: 'divider',
 | 
			
		||||
    direction: 'horizontal',
 | 
			
		||||
@ -97,6 +77,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '置顶',
 | 
			
		||||
    icon: markRaw(Top),
 | 
			
		||||
    display: () => !isPage(node.value) && !props.isMultiSelect,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      editorService?.moveLayer(LayerOffset.TOP);
 | 
			
		||||
@ -105,25 +86,19 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '置底',
 | 
			
		||||
    icon: markRaw(Bottom),
 | 
			
		||||
    display: () => !isPage(node.value) && !props.isMultiSelect,
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      editorService?.moveLayer(LayerOffset.BOTTOM);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  useMoveToMenu(services),
 | 
			
		||||
  {
 | 
			
		||||
    type: 'divider',
 | 
			
		||||
    direction: 'horizontal',
 | 
			
		||||
    display: () => !isPage(node.value) && !props.isMultiSelect,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '删除',
 | 
			
		||||
    icon: Delete,
 | 
			
		||||
    display: () => !isPage(node.value),
 | 
			
		||||
    handler: () => {
 | 
			
		||||
      nodes.value && editorService?.remove(nodes.value);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  useDeleteMenu(),
 | 
			
		||||
  {
 | 
			
		||||
    type: 'divider',
 | 
			
		||||
    direction: 'horizontal',
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  padding: 4px 0px;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  max-height: 80%;
 | 
			
		||||
 | 
			
		||||
  .menu-item {
 | 
			
		||||
    color: #333;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										92
									
								
								packages/editor/src/utils/content-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								packages/editor/src/utils/content-menu.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
			
		||||
import { computed, markRaw, Ref, ref } from 'vue';
 | 
			
		||||
import { CopyDocument, Delete, DocumentCopy } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import { Id, MContainer, NodeType } from '@tmagic/schema';
 | 
			
		||||
import { isPage } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import type { MenuButton, Services } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
export const useDeleteMenu = (): MenuButton => ({
 | 
			
		||||
  type: 'button',
 | 
			
		||||
  text: '删除',
 | 
			
		||||
  icon: Delete,
 | 
			
		||||
  display: (services) => {
 | 
			
		||||
    const node = services?.editorService?.get('node');
 | 
			
		||||
    return node?.type !== NodeType.ROOT && !isPage(node);
 | 
			
		||||
  },
 | 
			
		||||
  handler: (services) => {
 | 
			
		||||
    const nodes = services?.editorService?.get('nodes');
 | 
			
		||||
    nodes && services?.editorService?.remove(nodes);
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const canPaste = ref(false);
 | 
			
		||||
 | 
			
		||||
export const useCopyMenu = (): MenuButton => ({
 | 
			
		||||
  type: 'button',
 | 
			
		||||
  text: '复制',
 | 
			
		||||
  icon: markRaw(CopyDocument),
 | 
			
		||||
  handler: (services) => {
 | 
			
		||||
    const nodes = services?.editorService?.get('nodes');
 | 
			
		||||
    nodes && services?.editorService?.copy(nodes);
 | 
			
		||||
    canPaste.value = true;
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const usePasteMenu = (menu?: Ref<InstanceType<typeof ContentMenu> | undefined>): MenuButton => ({
 | 
			
		||||
  type: 'button',
 | 
			
		||||
  text: '粘贴',
 | 
			
		||||
  icon: markRaw(DocumentCopy),
 | 
			
		||||
  display: () => canPaste.value,
 | 
			
		||||
  handler: (services) => {
 | 
			
		||||
    const nodes = services?.editorService?.get('nodes');
 | 
			
		||||
    if (!nodes || nodes.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    if (menu?.value?.$el) {
 | 
			
		||||
      const stage = services?.editorService?.get('stage');
 | 
			
		||||
      const rect = menu.value.$el.getBoundingClientRect();
 | 
			
		||||
      const parentRect = stage?.container?.getBoundingClientRect();
 | 
			
		||||
      const initialLeft = (rect.left || 0) - (parentRect?.left || 0);
 | 
			
		||||
      const initialTop = (rect.top || 0) - (parentRect?.top || 0);
 | 
			
		||||
      services?.editorService?.paste({ left: initialLeft, top: initialTop });
 | 
			
		||||
    } else {
 | 
			
		||||
      services?.editorService?.paste();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const moveTo = (id: Id, services?: Services) => {
 | 
			
		||||
  if (!services?.editorService) return;
 | 
			
		||||
 | 
			
		||||
  const nodes = services.editorService.get('nodes') || [];
 | 
			
		||||
  const parent = services.editorService.getNodeById(id) as MContainer;
 | 
			
		||||
 | 
			
		||||
  if (!parent) return;
 | 
			
		||||
 | 
			
		||||
  services?.editorService.add(nodes, parent);
 | 
			
		||||
  services?.editorService.remove(nodes);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useMoveToMenu = (services?: Services): MenuButton => {
 | 
			
		||||
  const root = computed(() => services?.editorService?.get('root'));
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'button',
 | 
			
		||||
    text: '移动至',
 | 
			
		||||
    display: (services) => {
 | 
			
		||||
      const node = services?.editorService?.get('node');
 | 
			
		||||
      const pageLength = services?.editorService?.get('pageLength') || 0;
 | 
			
		||||
      return !isPage(node) && pageLength > 1;
 | 
			
		||||
    },
 | 
			
		||||
    items: (root.value?.items || [])
 | 
			
		||||
      .filter((page) => page.id !== services?.editorService?.get('page')?.id)
 | 
			
		||||
      .map((page) => ({
 | 
			
		||||
        text: `${page.name}(${page.id})`,
 | 
			
		||||
        type: 'button',
 | 
			
		||||
        handler: (services?: Services) => {
 | 
			
		||||
          moveTo(page.id, services);
 | 
			
		||||
        },
 | 
			
		||||
      })),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user