mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-04 15:52:13 +08:00
将 pageSteps/codeBlockState/dataSourceState 三套独立历史栈收敛为统一的 steps 结构 (按 stepType 分桶),并新增 registerStepType/setStepName/getStepName 支持自定义 扩展历史类型。同步重构 history 相关服务、组件、工具方法、测试与文档。
220 lines
6.1 KiB
Vue
220 lines
6.1 KiB
Vue
<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.canUndo('page', editorService.get('page')?.id),
|
||
handler: () => editorService.undo(),
|
||
});
|
||
break;
|
||
case 'redo':
|
||
config.push({
|
||
type: 'button',
|
||
className: 'redo',
|
||
icon: markRaw(Right),
|
||
tooltip: `前进(${ctrl}+Shift+z)`,
|
||
disabled: () => !historyService.canRedo('page', editorService.get('page')?.id),
|
||
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>
|