refactor(editor): 重构右键菜单

This commit is contained in:
roymondchen 2022-05-05 20:22:50 +08:00 committed by jia000
parent 34eb57b37a
commit 56375b0fb0
8 changed files with 425 additions and 290 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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