mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
refactor(editor): 重构右键菜单
This commit is contained in:
parent
34eb57b37a
commit
56375b0fb0
107
packages/editor/src/components/ContentMenu.vue
Normal file
107
packages/editor/src/components/ContentMenu.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div v-if="menuData.length" v-show="visible" class="magic-editor-content-menu" ref="menu" :style="menuStyle">
|
||||
<div>
|
||||
<tool-button
|
||||
v-for="(item, index) in menuData"
|
||||
event-type="mouseup"
|
||||
:data="item"
|
||||
:key="index"
|
||||
@mouseup="hide"
|
||||
@mouseenter="showSubMenu(item)"
|
||||
></tool-button>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<content-menu class="sub-menu" ref="subMenu" :menu-data="subMenuData"></content-menu>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, onMounted, PropType, ref } from 'vue';
|
||||
|
||||
import { MenuButton, MenuItem } from '@editor/type';
|
||||
|
||||
import ToolButton from './ToolButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ToolButton },
|
||||
|
||||
props: {
|
||||
menuData: {
|
||||
type: Array as PropType<MenuItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const menu = ref<HTMLDivElement>();
|
||||
const subMenu = ref<any>();
|
||||
const visible = ref(false);
|
||||
const subMenuData = ref<MenuItem[]>([]);
|
||||
const menuStyle = ref({
|
||||
left: '0',
|
||||
top: '0',
|
||||
});
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
globalThis.addEventListener(
|
||||
'mousedown',
|
||||
(e: MouseEvent) => {
|
||||
if (!visible.value || (e.target && menu.value?.contains(e.target as HTMLElement))) {
|
||||
return;
|
||||
}
|
||||
hide();
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
menu,
|
||||
subMenu,
|
||||
visible,
|
||||
menuStyle,
|
||||
subMenuData,
|
||||
|
||||
hide,
|
||||
|
||||
show(e: MouseEvent) {
|
||||
visible.value = true;
|
||||
|
||||
nextTick(() => {
|
||||
const menuHeight = menu.value?.clientHeight || 0;
|
||||
|
||||
let top = e.clientY;
|
||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||
top = document.body.clientHeight - menuHeight;
|
||||
}
|
||||
|
||||
menuStyle.value = {
|
||||
top: `${top}px`,
|
||||
left: `${e.clientX}px`,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
showSubMenu(item: MenuItem) {
|
||||
const menuItem = item as MenuButton;
|
||||
if (typeof item !== 'object' || !menuItem.items?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
subMenuData.value = menuItem.items;
|
||||
if (menu.value) {
|
||||
subMenu.value.show({
|
||||
clientX: menu.value.offsetLeft + menu.value.clientWidth,
|
||||
clientY: menu.value.offsetTop,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@ -1,24 +1,37 @@
|
||||
<template>
|
||||
<div v-if="display" class="menu-item">
|
||||
<el-divider v-if="item.type === 'divider'" direction="vertical"></el-divider>
|
||||
<div
|
||||
v-if="display"
|
||||
class="menu-item"
|
||||
:class="item.type"
|
||||
@click="clickHandler(item, $event)"
|
||||
@mousedown="mousedownHandler(item, $event)"
|
||||
@mouseup="mouseupHandler(item, $event)"
|
||||
>
|
||||
<el-divider v-if="item.type === 'divider'" :direction="item.direction || 'vertical'"></el-divider>
|
||||
<div v-else-if="item.type === 'text'" class="menu-item-text">{{ item.text }}</div>
|
||||
|
||||
<template v-else-if="item.type === 'zoom'">
|
||||
<el-button size="small" type="text"><m-icon :icon="ZoomIn" @click="zoomInHandler"></m-icon></el-button>
|
||||
<tool-button
|
||||
:data="{ type: 'button', icon: ZoomIn, handler: zoomInHandler, tooltip: '放大' }"
|
||||
:event-type="eventType"
|
||||
></tool-button>
|
||||
<span class="menu-item-text" style="margin: 0 5px">{{ parseInt(`${zoom * 100}`, 10) }}%</span>
|
||||
<el-button size="small" type="text"><m-icon :icon="ZoomOut" @click="zoomOutHandler"></m-icon></el-button>
|
||||
<tool-button
|
||||
:data="{ type: 'button', icon: ZoomOut, handler: zoomOutHandler, tooltip: '缩小' }"
|
||||
:event-type="eventType"
|
||||
></tool-button>
|
||||
</template>
|
||||
|
||||
<el-tooltip
|
||||
v-else-if="item.type === 'button'"
|
||||
effect="dark"
|
||||
placement="bottom-start"
|
||||
:content="item.tooltip || item.text"
|
||||
>
|
||||
<el-button size="small" type="text" :disabled="disabled" @click="buttonHandler(item)"
|
||||
><m-icon :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
||||
<template v-else-if="item.type === 'button'">
|
||||
<el-tooltip v-if="item.tooltip" effect="dark" placement="bottom-start" :content="item.tooltip">
|
||||
<el-button size="small" type="text" :disabled="disabled"
|
||||
><m-icon v-if="item.icon" :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
<el-button v-else size="small" type="text" :disabled="disabled"
|
||||
><m-icon v-if="item.icon" :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<el-dropdown v-else-if="item.type === 'dropdown'" trigger="click" :disabled="disabled" @command="dropdownHandler">
|
||||
<span class="el-dropdown-link menubar-menu-button">
|
||||
@ -38,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, PropType } from 'vue';
|
||||
import { computed, defineComponent, inject, markRaw, PropType } from 'vue';
|
||||
import { ArrowDown, Back, Delete, Grid, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons';
|
||||
|
||||
import { NodeType } from '@tmagic/schema';
|
||||
@ -58,6 +71,11 @@ export default defineComponent({
|
||||
display: false,
|
||||
}),
|
||||
},
|
||||
|
||||
eventType: {
|
||||
type: String as PropType<'mousedown' | 'mouseup' | 'click'>,
|
||||
default: 'click',
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
@ -87,7 +105,7 @@ export default defineComponent({
|
||||
case 'delete':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: Delete,
|
||||
icon: markRaw(Delete),
|
||||
tooltip: '刪除',
|
||||
disabled: () => services?.editorService.get('node')?.type === NodeType.PAGE,
|
||||
handler: () => services?.editorService.remove(services?.editorService.get('node')),
|
||||
@ -95,7 +113,7 @@ export default defineComponent({
|
||||
case 'undo':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: Back,
|
||||
icon: markRaw(Back),
|
||||
tooltip: '后退',
|
||||
disabled: () => !services?.historyService.state.canUndo,
|
||||
handler: () => services?.editorService.undo(),
|
||||
@ -103,7 +121,7 @@ export default defineComponent({
|
||||
case 'redo':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: Right,
|
||||
icon: markRaw(Right),
|
||||
tooltip: '前进',
|
||||
disabled: () => !services?.historyService.state.canRedo,
|
||||
handler: () => services?.editorService.redo(),
|
||||
@ -111,28 +129,28 @@ export default defineComponent({
|
||||
case 'zoom-in':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: ZoomIn,
|
||||
icon: markRaw(ZoomIn),
|
||||
tooltip: '放大',
|
||||
handler: zoomInHandler,
|
||||
};
|
||||
case 'zoom-out':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: ZoomOut,
|
||||
icon: markRaw(ZoomOut),
|
||||
tooltip: '縮小',
|
||||
handler: zoomOutHandler,
|
||||
};
|
||||
case 'rule':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: ScaleToOriginal,
|
||||
icon: markRaw(ScaleToOriginal),
|
||||
tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
|
||||
handler: () => uiService?.set('showRule', !showRule.value),
|
||||
};
|
||||
case 'guides':
|
||||
return {
|
||||
type: 'button',
|
||||
icon: Grid,
|
||||
icon: markRaw(Grid),
|
||||
tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
|
||||
handler: () => uiService?.set('showGuides', !showGuides.value),
|
||||
};
|
||||
@ -153,9 +171,16 @@ export default defineComponent({
|
||||
return item.value.disabled;
|
||||
});
|
||||
|
||||
const buttonHandler = (item: MenuButton | MenuComponent, event: MouseEvent) => {
|
||||
if (disabled.value) return;
|
||||
if (typeof (item as MenuButton).handler === 'function' && services) {
|
||||
(item as MenuButton).handler?.(services, event);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
ZoomIn: markRaw(ZoomIn),
|
||||
ZoomOut: markRaw(ZoomOut),
|
||||
|
||||
item,
|
||||
zoom,
|
||||
@ -178,10 +203,24 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
buttonHandler(item: MenuButton | MenuComponent) {
|
||||
if (disabled.value) return;
|
||||
if (typeof (item as MenuButton).handler === 'function') {
|
||||
(item as MenuButton).handler?.(services);
|
||||
clickHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||
if (props.eventType !== 'click') return;
|
||||
if (item.type === 'button') {
|
||||
buttonHandler(item, event);
|
||||
}
|
||||
},
|
||||
|
||||
mousedownHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||
if (props.eventType !== 'mousedown') return;
|
||||
if (item.type === 'button') {
|
||||
buttonHandler(item, event);
|
||||
}
|
||||
},
|
||||
|
||||
mouseupHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||
if (props.eventType !== 'mouseup') return;
|
||||
if (item.type === 'button') {
|
||||
buttonHandler(item, event);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,91 +1,104 @@
|
||||
<template>
|
||||
<div v-if="node" class="magic-editor-content-menu">
|
||||
<div
|
||||
v-if="node.items"
|
||||
class="magic-editor-content-menu-item"
|
||||
@mouseenter="setSubVisitable(true)"
|
||||
@mouseleave="setSubVisitable(false)"
|
||||
>
|
||||
新增
|
||||
</div>
|
||||
<div v-if="node.type !== 'app'" class="magic-editor-content-menu-item" @click="() => copy(node)">复制</div>
|
||||
<div
|
||||
v-if="node.type !== 'app' && node.type !== 'page'"
|
||||
class="magic-editor-content-menu-item"
|
||||
@click="() => remove()"
|
||||
>
|
||||
删除
|
||||
</div>
|
||||
<div class="subMenu" v-show="subVisible" @mouseenter="setSubVisitable(true)" @mouseleave="setSubVisitable(false)">
|
||||
<el-scrollbar>
|
||||
<template v-if="node.type === 'tabs'">
|
||||
<div
|
||||
class="magic-editor-content-menu-item"
|
||||
@click="
|
||||
() =>
|
||||
append({
|
||||
type: 'tab-pane',
|
||||
})
|
||||
"
|
||||
>
|
||||
标签
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="node.items">
|
||||
<div v-for="list in componentGroupList" :key="list.title">
|
||||
<template v-for="item in list.items">
|
||||
<div class="magic-editor-content-menu-item" v-if="item" :key="item.type" @click="() => append(item)">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="separation"></div>
|
||||
</div>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<content-menu :menu-data="menuData" ref="menu" style="overflow: initial"></content-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, ref } from 'vue';
|
||||
|
||||
import type { MNode } from '@tmagic/schema';
|
||||
import { NodeType } from '@tmagic/schema';
|
||||
|
||||
import type { AddMNode, Services } from '@editor/type';
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
import type { ComponentGroup, MenuButton, MenuItem, Services } from '@editor/type';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'magic-editor-content-menu',
|
||||
components: { ContentMenu },
|
||||
|
||||
setup() {
|
||||
const services = inject<Services>('services');
|
||||
const subVisible = ref(false);
|
||||
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||
const node = computed(() => services?.editorService.get('node'));
|
||||
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[] =>
|
||||
group.items.map((component) => ({
|
||||
text: component.text,
|
||||
type: 'button',
|
||||
handler: () => {
|
||||
services?.editorService.add({
|
||||
name: component.text,
|
||||
type: component.type,
|
||||
...(component.data || {}),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const getSubMenuData = computed<MenuButton[]>(() => {
|
||||
if (node.value?.type === 'tabs') {
|
||||
return [
|
||||
{
|
||||
text: '标签页',
|
||||
type: 'button',
|
||||
handler: () => {
|
||||
services?.editorService.add({
|
||||
type: 'tab-pane',
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (node.value?.items) {
|
||||
return (
|
||||
componentList.value.reduce(
|
||||
(subMenuData: MenuButton[], group: ComponentGroup, index) =>
|
||||
subMenuData.concat(
|
||||
createMenuItems(group),
|
||||
index < componentList.value.length - 1
|
||||
? [
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
[],
|
||||
) || []
|
||||
);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return {
|
||||
subVisible,
|
||||
menu,
|
||||
menuData: computed<MenuItem[]>(() => [
|
||||
{
|
||||
type: 'button',
|
||||
text: '新增',
|
||||
display: () => node.value?.items?.length > 0,
|
||||
items: getSubMenuData.value,
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '复制',
|
||||
display: () => !isRoot.value,
|
||||
handler: () => {
|
||||
node.value && services?.editorService.copy(node.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '删除',
|
||||
display: () => !isRoot.value && !isPage.value,
|
||||
handler: () => {
|
||||
node.value && services?.editorService.remove(node.value);
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
node,
|
||||
|
||||
componentGroupList: computed(() => services?.componentListService.getList()),
|
||||
|
||||
append(config: AddMNode) {
|
||||
services?.editorService.add({
|
||||
name: config.text,
|
||||
type: config.type,
|
||||
...(config.data || {}),
|
||||
});
|
||||
},
|
||||
|
||||
remove() {
|
||||
node.value && services?.editorService.remove(node.value);
|
||||
},
|
||||
|
||||
copy(node?: MNode) {
|
||||
node && services?.editorService.copy(node);
|
||||
},
|
||||
|
||||
setSubVisitable(v: boolean) {
|
||||
subVisible.value = v;
|
||||
show(e: MouseEvent) {
|
||||
menu.value?.show(e);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -33,8 +33,8 @@
|
||||
<div
|
||||
:id="data.id"
|
||||
class="cus-tree-node"
|
||||
@mousedown="toogleClickFlag"
|
||||
@mouseup="toogleClickFlag"
|
||||
@mousedown="toggleClickFlag"
|
||||
@mouseup="toggleClickFlag"
|
||||
@mouseenter="highlightHandler(data)"
|
||||
:class="{ 'cus-tree-node-hover': canHighlight && data.id === highlightNode?.id }"
|
||||
>
|
||||
@ -48,13 +48,13 @@
|
||||
</el-tree>
|
||||
|
||||
<teleport to="body">
|
||||
<layer-menu :style="menuStyle"></layer-menu>
|
||||
<layer-menu ref="menu"></layer-menu>
|
||||
</teleport>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, onMounted, Ref, ref, watchEffect } from 'vue';
|
||||
import { computed, defineComponent, inject, Ref, ref, watchEffect } from 'vue';
|
||||
import type { ElTree } from 'element-plus';
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
@ -169,46 +169,6 @@ const useFilter = (tree: Ref<InstanceType<typeof ElTree> | undefined>) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const useContentMenu = (editorService?: EditorService) => {
|
||||
const menuStyle = ref({
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
menuStyle.value.display = 'none';
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
menuStyle,
|
||||
|
||||
contextmenu(event: MouseEvent, data: MNode) {
|
||||
const bodyHeight = globalThis.document.body.clientHeight;
|
||||
|
||||
const left = `${event.clientX + 20}px`;
|
||||
let top = `${event.clientY - 10}px`;
|
||||
|
||||
if (event.clientY + 300 > bodyHeight) {
|
||||
top = `${bodyHeight - 300}px`;
|
||||
}
|
||||
|
||||
menuStyle.value.left = left;
|
||||
menuStyle.value.top = top;
|
||||
menuStyle.value.display = '';
|
||||
|
||||
select(data, editorService);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'magic-editor-layer-panel',
|
||||
|
||||
@ -217,13 +177,14 @@ export default defineComponent({
|
||||
setup() {
|
||||
const services = inject<Services>('services');
|
||||
const tree = ref<InstanceType<typeof ElTree>>();
|
||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||
const clicked = ref(false);
|
||||
const editorService = services?.editorService;
|
||||
const highlightHandler = throttle((data: MNode) => {
|
||||
highlight(data, editorService);
|
||||
}, throttleTime);
|
||||
|
||||
const toogleClickFlag = () => {
|
||||
const toggleClickFlag = () => {
|
||||
clicked.value = !clicked.value;
|
||||
};
|
||||
|
||||
@ -234,10 +195,14 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
tree,
|
||||
menu,
|
||||
...statusData,
|
||||
...useDrop(tree, editorService),
|
||||
...useFilter(tree),
|
||||
...useContentMenu(editorService),
|
||||
|
||||
highlightHandler,
|
||||
toggleClickFlag,
|
||||
canHighlight,
|
||||
|
||||
clickHandler(data: MNode): void {
|
||||
if (services?.uiService.get<boolean>('uiSelectMode')) {
|
||||
@ -247,9 +212,12 @@ export default defineComponent({
|
||||
tree.value?.setCurrentKey(data.id);
|
||||
select(data, editorService);
|
||||
},
|
||||
highlightHandler,
|
||||
toogleClickFlag,
|
||||
canHighlight,
|
||||
|
||||
async contextmenu(event: MouseEvent, data: MNode) {
|
||||
event.preventDefault();
|
||||
await select(data, editorService);
|
||||
menu.value?.show(event);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -13,7 +13,7 @@
|
||||
:style="`transform: scale(${zoom})`"
|
||||
></div>
|
||||
<teleport to="body">
|
||||
<viewer-menu ref="menu" :style="menuStyle"></viewer-menu>
|
||||
<viewer-menu ref="menu"></viewer-menu>
|
||||
</teleport>
|
||||
</scroll-viewer>
|
||||
</template>
|
||||
@ -43,45 +43,6 @@ import type { Services, StageRect } from '@editor/type';
|
||||
|
||||
import ViewerMenu from './ViewerMenu.vue';
|
||||
|
||||
const useMenu = () => {
|
||||
const menu = ref<InstanceType<typeof ViewerMenu>>();
|
||||
const menuStyle = ref({
|
||||
display: 'none',
|
||||
left: '0',
|
||||
top: '0',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
menuStyle.value.display = 'none';
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
menu,
|
||||
menuStyle,
|
||||
|
||||
contextmenuHandler(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const menuHeight = menu.value?.$el.clientHeight;
|
||||
let top = e.clientY;
|
||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||
top = document.body.clientHeight - menuHeight;
|
||||
}
|
||||
menuStyle.value = {
|
||||
display: 'block',
|
||||
top: `${top}px`,
|
||||
left: `${e.clientX}px`,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'magic-stage',
|
||||
|
||||
@ -115,12 +76,13 @@ export default defineComponent({
|
||||
|
||||
const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
|
||||
const stageContainer = ref<HTMLDivElement>();
|
||||
const menu = ref<InstanceType<typeof ViewerMenu>>();
|
||||
|
||||
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
||||
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
|
||||
const root = computed(() => services?.editorService.get<MApp>('root'));
|
||||
const page = computed(() => services?.editorService.get<MPage>('page'));
|
||||
const zoom = computed(() => services?.uiService.get<number>('zoom'));
|
||||
const zoom = computed(() => services?.uiService.get<number>('zoom') || 1);
|
||||
const node = computed(() => services?.editorService.get<MNode>('node'));
|
||||
|
||||
let stage: StageCore | null = null;
|
||||
@ -218,9 +180,14 @@ export default defineComponent({
|
||||
return {
|
||||
stageWrap,
|
||||
stageContainer,
|
||||
menu,
|
||||
stageRect,
|
||||
zoom,
|
||||
...useMenu(),
|
||||
|
||||
contextmenuHandler(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
menu.value?.show(e);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1,47 +1,31 @@
|
||||
<template>
|
||||
<div class="magic-editor-content-menu" ref="menu">
|
||||
<div>
|
||||
<div class="magic-editor-content-menu-item" @click="() => center()" v-if="canCenter">水平居中</div>
|
||||
<div class="magic-editor-content-menu-item" @click="() => copy()">复制</div>
|
||||
<div class="magic-editor-content-menu-item" @click="paste" v-if="canPaste">粘贴</div>
|
||||
<template v-if="canMoveZPos">
|
||||
<div class="separation"></div>
|
||||
<div class="magic-editor-content-menu-item" @click="topItem">上移一层</div>
|
||||
<div class="magic-editor-content-menu-item" @click="bottomItem">下移一层</div>
|
||||
<div class="magic-editor-content-menu-item" @click="top">置顶</div>
|
||||
<div class="magic-editor-content-menu-item" @click="bottom">置底</div>
|
||||
</template>
|
||||
<template v-if="canDelete">
|
||||
<div class="separation"></div>
|
||||
<div class="magic-editor-content-menu-item" @click="() => remove()">删除</div>
|
||||
</template>
|
||||
<div class="separation"></div>
|
||||
<div class="magic-editor-content-menu-item" @click="clearGuides">清空参考线</div>
|
||||
</div>
|
||||
</div>
|
||||
<content-menu :menu-data="menuData" ref="menu"></content-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, onMounted, ref, watch } from 'vue';
|
||||
import { computed, defineComponent, inject, markRaw, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { Bottom, Delete, DocumentCopy, Top } from '@element-plus/icons';
|
||||
|
||||
import { NodeType } from '@tmagic/schema';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
|
||||
import { LayerOffset, Layout, Services } from '@editor/type';
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
import { LayerOffset, Layout, MenuItem, Services } from '@editor/type';
|
||||
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'magic-editor-ui-viewer-menu',
|
||||
components: { ContentMenu },
|
||||
|
||||
setup() {
|
||||
const services = inject<Services>('services');
|
||||
const editorService = services?.editorService;
|
||||
const menu = ref<HTMLDivElement>();
|
||||
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||
const canPaste = ref(false);
|
||||
const canCenter = ref(false);
|
||||
|
||||
const node = computed(() => editorService?.get('node'));
|
||||
const parent = computed(() => editorService?.get('parent'));
|
||||
const isPage = computed(() => node.value?.type === NodeType.PAGE);
|
||||
|
||||
onMounted(() => {
|
||||
const data = globalThis.localStorage.getItem(COPY_STORAGE_KEY);
|
||||
@ -62,49 +46,102 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
menu,
|
||||
canPaste,
|
||||
menuData: reactive<MenuItem[]>([
|
||||
{
|
||||
type: 'button',
|
||||
text: '水平居中',
|
||||
display: () => canCenter.value,
|
||||
handler: () => {
|
||||
node.value && editorService?.alignCenter(node.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '复制',
|
||||
icon: markRaw(DocumentCopy),
|
||||
handler: () => {
|
||||
node.value && editorService?.copy(node.value);
|
||||
canPaste.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '粘贴',
|
||||
display: () => canPaste.value,
|
||||
handler: () => {
|
||||
const top = menu.value?.$el.offsetTop || 0;
|
||||
const left = menu.value?.$el.offsetLeft || 0;
|
||||
editorService?.paste({ left, top });
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
display: () => !isPage.value,
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '上移一层',
|
||||
icon: markRaw(Top),
|
||||
display: () => !isPage.value,
|
||||
handler: () => {
|
||||
editorService?.moveLayer(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '下移一层',
|
||||
icon: markRaw(Bottom),
|
||||
display: () => !isPage.value,
|
||||
handler: () => {
|
||||
editorService?.moveLayer(-1);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '置顶',
|
||||
display: () => !isPage.value,
|
||||
handler: () => {
|
||||
editorService?.moveLayer(LayerOffset.TOP);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '置底',
|
||||
display: () => !isPage.value,
|
||||
handler: () => {
|
||||
editorService?.moveLayer(LayerOffset.BOTTOM);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
display: () => !isPage.value,
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '删除',
|
||||
icon: Delete,
|
||||
display: () => !isPage.value,
|
||||
handler: () => {
|
||||
node.value && editorService?.remove(node.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '清空参考线',
|
||||
handler: () => {
|
||||
editorService?.get<StageCore>('stage').clearGuides();
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
canDelete: computed(() => node.value?.type !== NodeType.PAGE),
|
||||
canMoveZPos: computed(() => node.value?.type !== NodeType.PAGE),
|
||||
canCenter,
|
||||
|
||||
center() {
|
||||
node.value && editorService?.alignCenter(node.value);
|
||||
},
|
||||
|
||||
copy() {
|
||||
node.value && editorService?.copy(node.value);
|
||||
canPaste.value = true;
|
||||
},
|
||||
|
||||
paste() {
|
||||
const top = menu.value?.offsetTop || 0;
|
||||
const left = menu.value?.offsetLeft || 0;
|
||||
editorService?.paste({ left, top });
|
||||
},
|
||||
|
||||
remove() {
|
||||
node.value && editorService?.remove(node.value);
|
||||
},
|
||||
|
||||
top() {
|
||||
editorService?.moveLayer(LayerOffset.TOP);
|
||||
},
|
||||
|
||||
bottom() {
|
||||
editorService?.moveLayer(LayerOffset.BOTTOM);
|
||||
},
|
||||
|
||||
topItem() {
|
||||
editorService?.moveLayer(1);
|
||||
},
|
||||
|
||||
bottomItem() {
|
||||
editorService?.moveLayer(-1);
|
||||
},
|
||||
|
||||
clearGuides() {
|
||||
editorService?.get<StageCore>('stage').clearGuides();
|
||||
show(e: MouseEvent) {
|
||||
menu.value?.show(e);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -2,38 +2,45 @@
|
||||
position: fixed;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
||||
z-index: 9999;
|
||||
transform-origin: 0% 0%;
|
||||
font-weight: 600;
|
||||
padding: 4px 0px;
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
|
||||
.separation {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-color: rgba(155, 155, 155, 0.1);
|
||||
}
|
||||
|
||||
.subMenu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
transform: translate(100%, 0);
|
||||
background-color: #fff;
|
||||
.menu-item {
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
||||
top: 0;
|
||||
height: 223px;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
transition: all 0.2s ease 0s;
|
||||
padding: 5px 14px;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.el-button--text,
|
||||
i {
|
||||
color: $--font-color;
|
||||
}
|
||||
|
||||
&.divider {
|
||||
padding: 0 14px;
|
||||
|
||||
.el-divider {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f5f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.magic-editor-content-menu-item {
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
transition: all 0.2s ease 0s;
|
||||
padding: 10px 14px;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
@ -125,6 +125,8 @@ export interface MenuButton {
|
||||
* zoom: 放大缩小
|
||||
*/
|
||||
type: 'button' | 'dropdown' | 'text' | 'divider' | 'zoom';
|
||||
/** 当type为divider时有效,分割线方向, 默认vertical */
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
/** 展示的文案 */
|
||||
text?: string;
|
||||
/** 鼠标悬浮是显示的气泡中的文案 */
|
||||
@ -136,14 +138,9 @@ export interface MenuButton {
|
||||
/** 是否显示,默认为true */
|
||||
display?: boolean | ((data?: Services) => boolean);
|
||||
/** type为button/dropdown时点击运行的方法 */
|
||||
handler?: (data?: Services) => Promise<any> | any;
|
||||
/** type为dropdown时,下拉的菜单列表 */
|
||||
items?: {
|
||||
/** 展示的文案 */
|
||||
text: string;
|
||||
/** 点击运行的方法 */
|
||||
handler(data: Services): any;
|
||||
}[];
|
||||
handler?: (data: Services, event: MouseEvent) => Promise<any> | any;
|
||||
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
|
||||
items?: MenuButton[];
|
||||
}
|
||||
|
||||
export interface MenuComponent {
|
||||
|
Loading…
x
Reference in New Issue
Block a user