2026-06-04 16:08:52 +08:00

220 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="m-editor-nav-menu" :style="{ height: `${height}px` }" ref="navMenu">
<NavMenuColumn
v-for="key in keys"
:key="key"
:column-key="key"
:items="buttons[key]"
:width="columnWidth?.[key]"
></NavMenuColumn>
</div>
</template>
<script lang="ts" setup>
import { computed, markRaw, onBeforeUnmount, onMounted, useTemplateRef } from 'vue';
import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
import { NodeType } from '@tmagic/core';
import { useServices } from '@editor/hooks/use-services';
import { ColumnLayout, MenuBarData, MenuButton, MenuComponent, MenuItem } from '@editor/type';
import HistoryListPanel from './history-list/HistoryListPanel.vue';
import NavMenuColumn from './NavMenuColumn.vue';
defineOptions({
name: 'MEditorNavMenu',
});
const props = withDefaults(
defineProps<{
data?: MenuBarData;
height?: number;
}>(),
{
data: () => ({}),
height: 35,
},
);
const { uiService, editorService, historyService } = useServices();
const columnWidth = computed(() => uiService.get('columnWidth'));
const keys = Object.values(ColumnLayout);
const showGuides = computed((): boolean => uiService.get('showGuides'));
const hasGuides = computed((): boolean => uiService.get('hasGuides'));
const showRule = computed((): boolean => uiService.get('showRule'));
const zoom = computed((): number => uiService.get('zoom'));
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
const ctrl = isMac ? 'Command' : 'Ctrl';
const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
if (typeof item !== 'string') {
return [item];
}
const config: (MenuButton | MenuComponent)[] = [];
switch (item) {
case '/':
config.push({
type: 'divider',
className: 'divider',
});
break;
case 'zoom':
config.push(
...getConfig('zoom-out'),
...getConfig(`${parseInt(`${zoom.value * 100}`, 10)}%`),
...getConfig('zoom-in'),
...getConfig('scale-to-original'),
...getConfig('scale-to-fit'),
);
break;
case 'delete':
config.push({
type: 'button',
className: 'delete',
icon: markRaw(Delete),
tooltip: '刪除(Delete)',
disabled: () => editorService.get('node')?.type === NodeType.PAGE,
handler: () => {
const node = editorService.get('node');
node && editorService.remove(node, { historySource: 'toolbar' });
},
});
break;
case 'undo':
config.push({
type: 'button',
className: 'undo',
icon: markRaw(Back),
tooltip: `后退(${ctrl}+z)`,
disabled: () => !historyService.state.canUndo,
handler: () => editorService.undo(),
});
break;
case 'redo':
config.push({
type: 'button',
className: 'redo',
icon: markRaw(Right),
tooltip: `前进(${ctrl}+Shift+z)`,
disabled: () => !historyService.state.canRedo,
handler: () => editorService.redo(),
});
break;
case 'history-list':
// 历史记录面板:以 component 形式挂入,自带 popover点击 nav 上的图标弹出。
config.push({
type: 'component',
className: 'history-list',
component: markRaw(HistoryListPanel),
});
break;
case 'zoom-in':
config.push({
type: 'button',
className: 'zoom-in',
icon: markRaw(ZoomIn),
tooltip: `放大(${ctrl}+=)`,
handler: () => uiService?.zoom(0.1),
});
break;
case 'zoom-out':
config.push({
type: 'button',
className: 'zoom-out',
icon: markRaw(ZoomOut),
tooltip: `縮小(${ctrl}+-)`,
handler: () => uiService?.zoom(-0.1),
});
break;
case 'scale-to-original':
config.push({
type: 'button',
className: 'scale-to-original',
icon: markRaw(ScaleToOriginal),
tooltip: `缩放到实际大小(${ctrl}+1)`,
handler: () => uiService?.set('zoom', 1),
});
break;
case 'scale-to-fit':
config.push({
type: 'button',
className: 'scale-to-fit',
icon: markRaw(FullScreen),
tooltip: `缩放以适应(${ctrl}+0)`,
handler: async () => uiService?.set('zoom', await uiService.calcZoom()),
});
break;
case 'rule':
config.push({
type: 'button',
className: 'rule',
icon: markRaw(Memo),
tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
handler: () => uiService?.set('showRule', !showRule.value),
});
break;
case 'guides':
if (!hasGuides.value) break;
config.push({
type: 'button',
className: 'guides',
icon: markRaw(Grid),
tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
handler: () => uiService?.set('showGuides', !showGuides.value),
});
break;
default:
config.push({
type: 'text',
text: item,
});
}
return config;
};
const buttons = computed(() => {
const data: {
[ColumnLayout.LEFT]: (MenuButton | MenuComponent)[];
[ColumnLayout.CENTER]: (MenuButton | MenuComponent)[];
[ColumnLayout.RIGHT]: (MenuButton | MenuComponent)[];
} = {
[ColumnLayout.LEFT]: [],
[ColumnLayout.CENTER]: [],
[ColumnLayout.RIGHT]: [],
};
keys.forEach((key) => {
const items = props.data[key] || [];
items.forEach((item) => {
data[key].push(...getConfig(item));
});
});
return data;
});
const navMenuEl = useTemplateRef<HTMLDivElement>('navMenu');
const resizeObserver = new ResizeObserver(() => {
const rect = navMenuEl.value?.getBoundingClientRect();
if (rect) {
uiService.set('navMenuRect', {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
});
}
});
onMounted(() => {
navMenuEl.value && resizeObserver.observe(navMenuEl.value);
});
onBeforeUnmount(() => {
resizeObserver.disconnect();
});
</script>