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