feat(editor): 数据源/代码编辑列表新增右键菜单

This commit is contained in:
roymondchen 2024-12-16 20:20:02 +08:00
parent 4f23aebd7f
commit 74f76d0ba3
16 changed files with 314 additions and 53 deletions

View File

@ -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 }) => {
// setTimeoutmouseupmouseup
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,
});
}

View File

@ -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);
}
};

View File

@ -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,
};

View File

@ -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: {},
},

View File

@ -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>

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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,
});

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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': [];

View File

@ -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');

View File

@ -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,

View File

@ -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,

View File

@ -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,
},
);

View File

@ -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)[];