mirror of
				https://github.com/Tencent/tmagic-editor.git
				synced 2025-11-04 10:49:51 +08:00 
			
		
		
		
	feat(editor): 数据源/代码编辑列表新增右键菜单
This commit is contained in:
		
							parent
							
								
									4f23aebd7f
								
							
						
					
					
						commit
						74f76d0ba3
					
				@ -6,6 +6,7 @@
 | 
			
		||||
      ref="menu"
 | 
			
		||||
      :style="menuStyle"
 | 
			
		||||
      @mouseenter="mouseenterHandler()"
 | 
			
		||||
      @contextmenu.prevent
 | 
			
		||||
    >
 | 
			
		||||
      <slot name="title"></slot>
 | 
			
		||||
      <div>
 | 
			
		||||
@ -82,8 +83,8 @@ const menuPosition = ref({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const menuStyle = computed(() => ({
 | 
			
		||||
  top: `${menuPosition.value.top}px`,
 | 
			
		||||
  left: `${menuPosition.value.left}px`,
 | 
			
		||||
  top: `${menuPosition.value.top + 2}px`,
 | 
			
		||||
  left: `${menuPosition.value.left + 2}px`,
 | 
			
		||||
  zIndex: curZIndex.value,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -98,10 +99,12 @@ const hide = () => {
 | 
			
		||||
  emit('hide');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const clickHandler = () => {
 | 
			
		||||
const clickHandler = (event: MouseEvent) => {
 | 
			
		||||
  if (!props.autoHide) return;
 | 
			
		||||
 | 
			
		||||
  hide();
 | 
			
		||||
  if (event.button === 0) {
 | 
			
		||||
    hide();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const outsideClickHideHandler = (e: MouseEvent) => {
 | 
			
		||||
@ -132,18 +135,15 @@ const setPosition = (e: { clientY: number; clientX: number }) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const show = (e?: { clientY: number; clientX: number }) => {
 | 
			
		||||
  // 加setTimeout是以为,如果菜单中的按钮监听的是mouseup,那么菜单显示出来时鼠标如果正好在菜单上就会马上触发按钮的mouseup
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    visible.value = true;
 | 
			
		||||
  visible.value = true;
 | 
			
		||||
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      e && setPosition(e);
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    e && setPosition(e);
 | 
			
		||||
 | 
			
		||||
      curZIndex.value = zIndex.nextZIndex();
 | 
			
		||||
    curZIndex.value = zIndex.nextZIndex();
 | 
			
		||||
 | 
			
		||||
      emit('show');
 | 
			
		||||
    });
 | 
			
		||||
  }, 300);
 | 
			
		||||
    emit('show');
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
 | 
			
		||||
@ -166,7 +166,7 @@ const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
 | 
			
		||||
        y = rect.top;
 | 
			
		||||
      }
 | 
			
		||||
      subMenu.value?.show({
 | 
			
		||||
        clientX: menu.value.offsetLeft + menu.value.clientWidth,
 | 
			
		||||
        clientX: menu.value.offsetLeft + menu.value.clientWidth - 2,
 | 
			
		||||
        clientY: y,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -131,7 +131,8 @@ const mousedownHandler = (item: MenuButton | MenuComponent, event: MouseEvent) =
 | 
			
		||||
 | 
			
		||||
const mouseupHandler = (item: MenuButton | MenuComponent, event: MouseEvent) => {
 | 
			
		||||
  if (props.eventType !== 'mouseup') return;
 | 
			
		||||
  if (item.type === 'button') {
 | 
			
		||||
 | 
			
		||||
  if (item.type === 'button' && event.button === 0) {
 | 
			
		||||
    buttonHandler(item, event);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ import { getIdFromEl } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ComponentGroup,
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  DatasourceTypeOption,
 | 
			
		||||
  MenuBarData,
 | 
			
		||||
  MenuButton,
 | 
			
		||||
@ -93,7 +94,7 @@ export interface EditorProps {
 | 
			
		||||
  /** 用于设置画布上的dom是否可以被拖入其中 */
 | 
			
		||||
  isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
 | 
			
		||||
  /** 用于自定义组件树与画布的右键菜单 */
 | 
			
		||||
  customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
  customContentMenu?: CustomContentMenuFunction;
 | 
			
		||||
  extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
 | 
			
		||||
  /** 页面顺序拖拽配置参数 */
 | 
			
		||||
  pageBarSortOptions?: PageBarSortOptions;
 | 
			
		||||
@ -123,4 +124,5 @@ export const defaultEditorProps = {
 | 
			
		||||
  canSelect: (el: HTMLElement) => Boolean(getIdFromEl()(el)),
 | 
			
		||||
  isContainer: (el: HTMLElement) => el.classList.contains('magic-ui-container'),
 | 
			
		||||
  codeOptions: () => ({}),
 | 
			
		||||
  customContentMenu: (menus: (MenuButton | MenuComponent)[]) => menus,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -158,6 +158,7 @@ import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height'
 | 
			
		||||
import { useFloatBox } from '@editor/hooks/use-float-box';
 | 
			
		||||
import {
 | 
			
		||||
  ColumnLayout,
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  type MenuButton,
 | 
			
		||||
  type MenuComponent,
 | 
			
		||||
  type Services,
 | 
			
		||||
@ -185,7 +186,7 @@ const props = withDefaults(
 | 
			
		||||
    layerContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
    indent?: number;
 | 
			
		||||
    nextLevelIndentIncrement?: number;
 | 
			
		||||
    customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    data: () => ({
 | 
			
		||||
@ -254,6 +255,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
 | 
			
		||||
      props: {
 | 
			
		||||
        indent: props.indent,
 | 
			
		||||
        nextLevelIndentIncrement: props.nextLevelIndentIncrement,
 | 
			
		||||
        customContentMenu: props.customContentMenu,
 | 
			
		||||
      },
 | 
			
		||||
      slots: {},
 | 
			
		||||
    },
 | 
			
		||||
@ -266,6 +268,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
 | 
			
		||||
      props: {
 | 
			
		||||
        indent: props.indent,
 | 
			
		||||
        nextLevelIndentIncrement: props.nextLevelIndentIncrement,
 | 
			
		||||
        customContentMenu: props.customContentMenu,
 | 
			
		||||
      },
 | 
			
		||||
      slots: {},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@
 | 
			
		||||
    :indent="indent"
 | 
			
		||||
    :next-level-indent-increment="nextLevelIndentIncrement"
 | 
			
		||||
    @node-click="clickHandler"
 | 
			
		||||
    @node-contextmenu="nodeContentMenuHandler"
 | 
			
		||||
  >
 | 
			
		||||
    <template #tree-node-label="{ data }">
 | 
			
		||||
      <div
 | 
			
		||||
@ -62,6 +63,7 @@ const props = defineProps<{
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  edit: [id: string];
 | 
			
		||||
  remove: [id: string];
 | 
			
		||||
  'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const services = inject<Services>('services');
 | 
			
		||||
@ -162,12 +164,21 @@ const deleteCode = async (id: string) => {
 | 
			
		||||
    if (typeof props.customError === 'function') {
 | 
			
		||||
      props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
 | 
			
		||||
    } else {
 | 
			
		||||
      tMagicMessage.error('代码块删除失败');
 | 
			
		||||
      if (existBinds) {
 | 
			
		||||
        tMagicMessage.error('代码块存在绑定关系,不可删除');
 | 
			
		||||
      } else {
 | 
			
		||||
        tMagicMessage.error('代码块不可删除');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nodeContentMenuHandler = (event: MouseEvent, data: TreeNodeData) => {
 | 
			
		||||
  emit('node-contextmenu', event, data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  filter: filterTextChangeHandler,
 | 
			
		||||
  deleteCode,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
      :next-level-indent-increment="nextLevelIndentIncrement"
 | 
			
		||||
      @edit="editCode"
 | 
			
		||||
      @remove="deleteCode"
 | 
			
		||||
      @node-contextmenu="nodeContentMenuHandler"
 | 
			
		||||
    >
 | 
			
		||||
      <template #code-block-panel-tool="{ id, data }">
 | 
			
		||||
        <slot name="code-block-panel-tool" :id="id" :data="data"></slot>
 | 
			
		||||
@ -32,6 +33,16 @@
 | 
			
		||||
    :content="codeConfig"
 | 
			
		||||
    @submit="submitCodeBlockHandler"
 | 
			
		||||
  ></CodeBlockEditor>
 | 
			
		||||
 | 
			
		||||
  <Teleport to="body">
 | 
			
		||||
    <ContentMenu
 | 
			
		||||
      v-if="menuData.length"
 | 
			
		||||
      :menu-data="menuData"
 | 
			
		||||
      ref="menu"
 | 
			
		||||
      style="overflow: initial"
 | 
			
		||||
      @hide="contentMenuHideHandler"
 | 
			
		||||
    ></ContentMenu>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
@ -41,11 +52,21 @@ import type { Id } from '@tmagic/core';
 | 
			
		||||
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
 | 
			
		||||
 | 
			
		||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import SearchInput from '@editor/components/SearchInput.vue';
 | 
			
		||||
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
 | 
			
		||||
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, EventBus, Services } from '@editor/type';
 | 
			
		||||
import type {
 | 
			
		||||
  CodeBlockListPanelSlots,
 | 
			
		||||
  CodeDeleteErrorType,
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  EventBus,
 | 
			
		||||
  MenuButton,
 | 
			
		||||
  MenuComponent,
 | 
			
		||||
  Services,
 | 
			
		||||
} from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import CodeBlockList from './CodeBlockList.vue';
 | 
			
		||||
import { useContentMenu } from './useContentMenu';
 | 
			
		||||
 | 
			
		||||
defineSlots<CodeBlockListPanelSlots>();
 | 
			
		||||
 | 
			
		||||
@ -53,10 +74,11 @@ defineOptions({
 | 
			
		||||
  name: 'MEditorCodeBlockListPanel',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  indent?: number;
 | 
			
		||||
  nextLevelIndentIncrement?: number;
 | 
			
		||||
  customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
 | 
			
		||||
  customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const eventBus = inject<EventBus>('eventBus');
 | 
			
		||||
@ -76,4 +98,13 @@ const filterTextChangeHandler = (val: string) => {
 | 
			
		||||
eventBus?.on('edit-code', (id: string) => {
 | 
			
		||||
  editCode(id);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  nodeContentMenuHandler,
 | 
			
		||||
  menuData: contentMenuData,
 | 
			
		||||
  contentMenuHideHandler,
 | 
			
		||||
} = useContentMenu((id: string) => {
 | 
			
		||||
  codeBlockList.value?.deleteCode(id);
 | 
			
		||||
});
 | 
			
		||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
import { inject, markRaw, useTemplateRef } from 'vue';
 | 
			
		||||
import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
 | 
			
		||||
import { cloneDeep } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import type { EventBus, MenuButton, MenuComponent, Services, TreeNodeData } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
export const useContentMenu = (deleteCode: (id: string) => void) => {
 | 
			
		||||
  const eventBus = inject<EventBus>('eventBus');
 | 
			
		||||
  const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
 | 
			
		||||
 | 
			
		||||
  let selectId = '';
 | 
			
		||||
 | 
			
		||||
  const menuData: (MenuButton | MenuComponent)[] = [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '编辑',
 | 
			
		||||
      icon: Edit,
 | 
			
		||||
      display: (services) => services?.codeBlockService?.getEditStatus() ?? true,
 | 
			
		||||
      handler: () => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        eventBus?.emit('edit-code', selectId);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '复制并粘贴至当前',
 | 
			
		||||
      icon: markRaw(CopyDocument),
 | 
			
		||||
      handler: async ({ codeBlockService }: Services) => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const codeBlock = codeBlockService.getCodeContentById(selectId);
 | 
			
		||||
        if (!codeBlock) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const newCodeId = await codeBlockService.getUniqueId();
 | 
			
		||||
 | 
			
		||||
        codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock));
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '删除',
 | 
			
		||||
      icon: Delete,
 | 
			
		||||
      handler: () => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        deleteCode(selectId);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const nodeContentMenuHandler = (event: MouseEvent, data: TreeNodeData) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (data.type === 'code') {
 | 
			
		||||
      menuRef.value?.show(event);
 | 
			
		||||
      if (data.id) {
 | 
			
		||||
        selectId = `${data.id}`;
 | 
			
		||||
      } else {
 | 
			
		||||
        selectId = '';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const contentMenuHideHandler = () => {
 | 
			
		||||
    selectId = '';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    menuData,
 | 
			
		||||
    nodeContentMenuHandler,
 | 
			
		||||
    contentMenuHideHandler,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -5,6 +5,7 @@
 | 
			
		||||
    :indent="indent"
 | 
			
		||||
    :next-level-indent-increment="nextLevelIndentIncrement"
 | 
			
		||||
    @node-click="clickHandler"
 | 
			
		||||
    @node-contextmenu="nodeContentMenuHandler"
 | 
			
		||||
  >
 | 
			
		||||
    <template #tree-node-label="{ data }">
 | 
			
		||||
      <div
 | 
			
		||||
@ -37,13 +38,13 @@ import { computed, inject } from 'vue';
 | 
			
		||||
import { Close, Edit, View } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import { DepData, DepTargetType, Id, MNode } from '@tmagic/core';
 | 
			
		||||
import { tMagicMessageBox, TMagicTag, TMagicTooltip } from '@tmagic/design';
 | 
			
		||||
import { TMagicTag, TMagicTooltip } from '@tmagic/design';
 | 
			
		||||
 | 
			
		||||
import Icon from '@editor/components/Icon.vue';
 | 
			
		||||
import Tree from '@editor/components/Tree.vue';
 | 
			
		||||
import { useFilter } from '@editor/hooks/use-filter';
 | 
			
		||||
import { useNodeStatus } from '@editor/hooks/use-node-status';
 | 
			
		||||
import type { DataSourceListSlots, Services } from '@editor/type';
 | 
			
		||||
import type { DataSourceListSlots, Services, TreeNodeData } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
defineSlots<DataSourceListSlots>();
 | 
			
		||||
 | 
			
		||||
@ -59,6 +60,7 @@ defineProps<{
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  edit: [id: string];
 | 
			
		||||
  remove: [id: string];
 | 
			
		||||
  'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const { depService, editorService, dataSourceService } = inject<Services>('services') || {};
 | 
			
		||||
@ -159,12 +161,6 @@ const editHandler = (id: string) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const removeHandler = async (id: string) => {
 | 
			
		||||
  await tMagicMessageBox.confirm('确定删除?', '提示', {
 | 
			
		||||
    confirmButtonText: '确定',
 | 
			
		||||
    cancelButtonText: '取消',
 | 
			
		||||
    type: 'warning',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  emit('remove', id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -181,6 +177,10 @@ const clickHandler = (event: MouseEvent, data: any) => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nodeContentMenuHandler = (event: MouseEvent, data: TreeNodeData) => {
 | 
			
		||||
  emit('node-contextmenu', event, data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  filter: filterTextChangeHandler,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@
 | 
			
		||||
      :next-level-indent-increment="nextLevelIndentIncrement"
 | 
			
		||||
      @edit="editHandler"
 | 
			
		||||
      @remove="removeHandler"
 | 
			
		||||
      @node-contextmenu="nodeContentMenuHandler"
 | 
			
		||||
    ></DataSourceList>
 | 
			
		||||
  </TMagicScrollbar>
 | 
			
		||||
 | 
			
		||||
@ -45,21 +46,40 @@
 | 
			
		||||
    :title="dialogTitle"
 | 
			
		||||
    @submit="submitDataSourceHandler"
 | 
			
		||||
  ></DataSourceConfigPanel>
 | 
			
		||||
 | 
			
		||||
  <Teleport to="body">
 | 
			
		||||
    <ContentMenu
 | 
			
		||||
      v-if="menuData.length"
 | 
			
		||||
      :menu-data="menuData"
 | 
			
		||||
      ref="menu"
 | 
			
		||||
      style="overflow: initial"
 | 
			
		||||
      @hide="contentMenuHideHandler"
 | 
			
		||||
    ></ContentMenu>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, inject, ref } from 'vue';
 | 
			
		||||
import { mergeWith } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
import { TMagicButton, TMagicPopover, TMagicScrollbar } from '@tmagic/design';
 | 
			
		||||
import { TMagicButton, tMagicMessageBox, TMagicPopover, TMagicScrollbar } from '@tmagic/design';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import SearchInput from '@editor/components/SearchInput.vue';
 | 
			
		||||
import ToolButton from '@editor/components/ToolButton.vue';
 | 
			
		||||
import { useDataSourceEdit } from '@editor/hooks/use-data-source-edit';
 | 
			
		||||
import type { DataSourceListSlots, EventBus, Services } from '@editor/type';
 | 
			
		||||
import type {
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  DataSourceListSlots,
 | 
			
		||||
  EventBus,
 | 
			
		||||
  MenuButton,
 | 
			
		||||
  MenuComponent,
 | 
			
		||||
  Services,
 | 
			
		||||
} from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import DataSourceConfigPanel from './DataSourceConfigPanel.vue';
 | 
			
		||||
import DataSourceList from './DataSourceList.vue';
 | 
			
		||||
import { useContentMenu } from './useContentMenu';
 | 
			
		||||
 | 
			
		||||
defineSlots<DataSourceListSlots>();
 | 
			
		||||
 | 
			
		||||
@ -67,9 +87,10 @@ defineOptions({
 | 
			
		||||
  name: 'MEditorDataSourceListPanel',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  indent?: number;
 | 
			
		||||
  nextLevelIndentIncrement?: number;
 | 
			
		||||
  customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const eventBus = inject<EventBus>('eventBus');
 | 
			
		||||
@ -105,7 +126,13 @@ const addHandler = (type: string) => {
 | 
			
		||||
  editDialog.value.show();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const removeHandler = (id: string) => {
 | 
			
		||||
const removeHandler = async (id: string) => {
 | 
			
		||||
  await tMagicMessageBox.confirm('确定删除?', '提示', {
 | 
			
		||||
    confirmButtonText: '确定',
 | 
			
		||||
    cancelButtonText: '取消',
 | 
			
		||||
    type: 'warning',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  dataSourceService?.remove(id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -118,4 +145,13 @@ const filterTextChangeHandler = (val: string) => {
 | 
			
		||||
eventBus?.on('edit-data-source', (id: string) => {
 | 
			
		||||
  editHandler(id);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
eventBus?.on('remove-data-source', (id: string) => {
 | 
			
		||||
  removeHandler(id);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler } = useContentMenu();
 | 
			
		||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
 | 
			
		||||
  props.customContentMenu(contentMenuData, 'data-source'),
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
import { inject, markRaw, useTemplateRef } from 'vue';
 | 
			
		||||
import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
 | 
			
		||||
import { cloneDeep } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import type { EventBus, MenuButton, MenuComponent, Services, TreeNodeData } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
export const useContentMenu = () => {
 | 
			
		||||
  const eventBus = inject<EventBus>('eventBus');
 | 
			
		||||
  const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
 | 
			
		||||
 | 
			
		||||
  let selectId = '';
 | 
			
		||||
 | 
			
		||||
  const menuData: (MenuButton | MenuComponent)[] = [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '编辑',
 | 
			
		||||
      icon: Edit,
 | 
			
		||||
      display: (services) => services?.dataSourceService?.get('editable') ?? true,
 | 
			
		||||
      handler: () => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        eventBus?.emit('edit-data-source', selectId);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '复制并粘贴至当前',
 | 
			
		||||
      icon: markRaw(CopyDocument),
 | 
			
		||||
      handler: ({ dataSourceService }: Services) => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const ds = dataSourceService.getDataSourceById(selectId);
 | 
			
		||||
        if (!ds) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        dataSourceService.add(cloneDeep(ds));
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'button',
 | 
			
		||||
      text: '删除',
 | 
			
		||||
      icon: Delete,
 | 
			
		||||
      handler: () => {
 | 
			
		||||
        if (!selectId) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        eventBus?.emit('remove-data-source', selectId);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const nodeContentMenuHandler = (event: MouseEvent, data: TreeNodeData) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (data.type === 'ds') {
 | 
			
		||||
      menuRef.value?.show(event);
 | 
			
		||||
      if (data.id) {
 | 
			
		||||
        selectId = `${data.id}`;
 | 
			
		||||
      } else {
 | 
			
		||||
        selectId = '';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const contentMenuHideHandler = () => {
 | 
			
		||||
    selectId = '';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    menuData,
 | 
			
		||||
    nodeContentMenuHandler,
 | 
			
		||||
    contentMenuHideHandler,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -10,23 +10,17 @@ import { isPage, isPageFragment } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import FolderMinusIcon from '@editor/icons/FolderMinusIcon.vue';
 | 
			
		||||
import type { ComponentGroup, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import type { ComponentGroup, CustomContentMenuFunction, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'MEditorLayerMenu',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    layerContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    layerContentMenu: () => [],
 | 
			
		||||
    customContentMenu: (menus: (MenuButton | MenuComponent)[]) => menus,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  layerContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
  customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  'collapse-all': [];
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,14 @@ import { TMagicScrollbar } from '@tmagic/design';
 | 
			
		||||
import SearchInput from '@editor/components/SearchInput.vue';
 | 
			
		||||
import Tree from '@editor/components/Tree.vue';
 | 
			
		||||
import { useFilter } from '@editor/hooks/use-filter';
 | 
			
		||||
import type { LayerPanelSlots, MenuButton, MenuComponent, Services, TreeNodeData } from '@editor/type';
 | 
			
		||||
import type {
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  LayerPanelSlots,
 | 
			
		||||
  MenuButton,
 | 
			
		||||
  MenuComponent,
 | 
			
		||||
  Services,
 | 
			
		||||
  TreeNodeData,
 | 
			
		||||
} from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import LayerMenu from './LayerMenu.vue';
 | 
			
		||||
import LayerNodeTool from './LayerNodeTool.vue';
 | 
			
		||||
@ -74,7 +81,7 @@ defineProps<{
 | 
			
		||||
  layerContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
  indent?: number;
 | 
			
		||||
  nextLevelIndentIncrement?: number;
 | 
			
		||||
  customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
  customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const services = inject<Services>('services');
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,14 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, inject } from 'vue';
 | 
			
		||||
 | 
			
		||||
import type { MenuButton, MenuComponent, Services, StageOptions, WorkspaceSlots } from '@editor/type';
 | 
			
		||||
import type {
 | 
			
		||||
  CustomContentMenuFunction,
 | 
			
		||||
  MenuButton,
 | 
			
		||||
  MenuComponent,
 | 
			
		||||
  Services,
 | 
			
		||||
  StageOptions,
 | 
			
		||||
  WorkspaceSlots,
 | 
			
		||||
} from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import MagicStage from './viewer/Stage.vue';
 | 
			
		||||
import Breadcrumb from './Breadcrumb.vue';
 | 
			
		||||
@ -34,7 +41,7 @@ withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    stageContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
    disabledStageOverlay?: boolean;
 | 
			
		||||
    customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    disabledStageOverlay: false,
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,8 @@ import { calcValueByFontsize, getIdFromEl } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import ScrollViewer from '@editor/components/ScrollViewer.vue';
 | 
			
		||||
import { useStage } from '@editor/hooks/use-stage';
 | 
			
		||||
import { DragType, Layout, type MenuButton, type MenuComponent, type Services, type StageOptions } from '@editor/type';
 | 
			
		||||
import type { CustomContentMenuFunction, MenuButton, MenuComponent, Services, StageOptions } from '@editor/type';
 | 
			
		||||
import { DragType, Layout } from '@editor/type';
 | 
			
		||||
import { getEditorConfig } from '@editor/utils/config';
 | 
			
		||||
import { KeyBindingContainerKey } from '@editor/utils/keybinding-config';
 | 
			
		||||
 | 
			
		||||
@ -80,7 +81,7 @@ const props = withDefaults(
 | 
			
		||||
    stageOptions: StageOptions;
 | 
			
		||||
    stageContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
    disabledStageOverlay?: boolean;
 | 
			
		||||
    customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    disabledStageOverlay: false,
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import { isPage, isPageFragment } from '@tmagic/utils';
 | 
			
		||||
 | 
			
		||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
			
		||||
import CenterIcon from '@editor/icons/CenterIcon.vue';
 | 
			
		||||
import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import { CustomContentMenuFunction, LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
			
		||||
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
@ -22,12 +22,10 @@ const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    isMultiSelect?: boolean;
 | 
			
		||||
    stageContentMenu: (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
 | 
			
		||||
    customContentMenu: CustomContentMenuFunction;
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    isMultiSelect: false,
 | 
			
		||||
    stageContentMenu: () => [],
 | 
			
		||||
    customContentMenu: (menus: (MenuButton | MenuComponent)[]) => menus,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -766,6 +766,7 @@ export type SyncHookPlugin<
 | 
			
		||||
 | 
			
		||||
export interface EventBusEvent {
 | 
			
		||||
  'edit-data-source': [id: string];
 | 
			
		||||
  'remove-data-source': [id: string];
 | 
			
		||||
  'edit-code': [id: string];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -787,3 +788,8 @@ export interface PageBarSortOptions extends PartSortableOptions {
 | 
			
		||||
  /** 在onStart之前调用 */
 | 
			
		||||
  beforeStart?: (event: SortableEvent, sortable: Sortable) => void | Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CustomContentMenuFunction = (
 | 
			
		||||
  menus: (MenuButton | MenuComponent)[],
 | 
			
		||||
  type: 'layer' | 'data-source' | 'viewer' | 'code-block',
 | 
			
		||||
) => (MenuButton | MenuComponent)[];
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user