roymondchen 4f284e8d9c feat(editor): 支持页面初始基线与 root 变更历史记录
设置 root 时为各页建立 initial 基线并展示在历史列表底部;编辑期间再次 set root 按页面粒度写入历史,并抽取历史工具函数以支持撤销下限与持久化恢复。
2026-06-11 15:00:11 +08:00

177 lines
5.4 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" ref="content" style="min-width: 900px">
<slot name="header"></slot>
<slot name="nav"></slot>
<slot name="content-before"></slot>
<slot name="src-code" v-if="showSrc">
<CodeEditor
class="m-editor-content"
editor-custom-type="m-editor-content"
:init-values="root"
:options="codeOptions"
@save="saveCode"
></CodeEditor>
</slot>
<SplitView
v-show="!showSrc"
ref="splitView"
class="m-editor-content"
left-class="m-editor-framework-left"
center-class="m-editor-framework-center"
right-class="m-editor-framework-right"
:left="hideSidebar ? undefined : columnWidth.left"
:right="columnWidth.right"
:min-left="hideSidebar ? 0 : MIN_LEFT_COLUMN_WIDTH"
:min-right="MIN_RIGHT_COLUMN_WIDTH"
:min-center="MIN_CENTER_COLUMN_WIDTH"
:width="frameworkRect.width"
@change="columnWidthChange"
>
<template v-if="!hideSidebar" #left>
<slot name="sidebar"></slot>
</template>
<template #center>
<slot v-if="page" name="workspace"></slot>
<slot v-else name="empty">
<AddPageBox :disabled-page-fragment="disabledPageFragment"></AddPageBox>
</slot>
<slot name="page-bar">
<PageBar
:disabled-page-fragment="disabledPageFragment"
:page-bar-sort-options="pageBarSortOptions"
:filter-function="pageFilterFunction"
>
<template #page-bar-add-button><slot name="page-bar-add-button"></slot></template>
<template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template>
<template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template>
<template #page-list-popover="{ list }"><slot name="page-list-popover" :list="list"></slot></template>
</PageBar>
</slot>
</template>
<template v-if="page" #right>
<slot name="props-panel"></slot>
</template>
</SplitView>
<slot name="content-after"></slot>
<slot name="footer"></slot>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue';
import type { MPage, MPageFragment } from '@tmagic/core';
import SplitView from '@editor/components/SplitView.vue';
import { useServices } from '@editor/hooks/use-services';
import { Protocol } from '@editor/services/storage';
import type { FrameworkSlots, GetColumnWidth, PageBarSortOptions } from '@editor/type';
import { getEditorConfig } from '@editor/utils/config';
import {
DEFAULT_LEFT_COLUMN_WIDTH,
LEFT_COLUMN_WIDTH_STORAGE_KEY,
MIN_CENTER_COLUMN_WIDTH,
MIN_LEFT_COLUMN_WIDTH,
MIN_RIGHT_COLUMN_WIDTH,
RIGHT_COLUMN_WIDTH_STORAGE_KEY,
} from '@editor/utils/const';
import PageBar from './page-bar/PageBar.vue';
import AddPageBox from './AddPageBox.vue';
import CodeEditor from './CodeEditor.vue';
defineSlots<FrameworkSlots>();
defineOptions({
name: 'MEditorFramework',
});
const props = defineProps<{
disabledPageFragment: boolean;
pageBarSortOptions?: PageBarSortOptions;
pageFilterFunction?: (_page: MPage | MPageFragment, _keyword: string) => boolean;
/** 是否隐藏左侧面板 */
hideSidebar?: boolean;
}>();
const codeOptions = inject('codeOptions', {});
const { editorService, uiService, storageService } = useServices();
const contentEl = useTemplateRef<HTMLDivElement>('content');
const splitViewRef = useTemplateRef<InstanceType<typeof SplitView>>('splitView');
const root = computed(() => editorService.get('root'));
const page = computed(() => editorService.get('page'));
const pageLength = computed(() => editorService.get('pageLength') || 0);
const showSrc = computed(() => uiService.get('showSrc'));
const columnWidth = computed(() => uiService.get('columnWidth'));
watch(pageLength, () => {
splitViewRef.value?.updateWidth();
});
watch(
() => uiService.get('hideSlideBar'),
(hideSlideBar) => {
uiService.set('columnWidth', {
...columnWidth.value,
left: hideSlideBar
? 0
: storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
DEFAULT_LEFT_COLUMN_WIDTH,
});
},
);
const columnWidthChange = (columnW: GetColumnWidth) => {
// 隐藏左侧面板时 left 恒为 0不覆盖已持久化的左栏宽度避免重新展示时丢失宽度
if (!props.hideSidebar) {
storageService.setItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, columnW.left, { protocol: Protocol.NUMBER });
}
storageService.setItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, columnW.right, { protocol: Protocol.NUMBER });
uiService.set('columnWidth', columnW);
};
const frameworkRect = computed(() => uiService.get('frameworkRect'));
const resizerObserver = new ResizeObserver((entries) => {
const { contentRect } = entries[0];
uiService.set('frameworkRect', {
width: contentRect.width,
height: contentRect.height,
left: contentRect.left,
top: contentRect.top,
});
});
onMounted(() => {
if (contentEl.value) {
resizerObserver.observe(contentEl.value);
}
});
onBeforeUnmount(() => {
resizerObserver.disconnect();
});
const saveCode = (value: string) => {
try {
const parseDSL = getEditorConfig('parseDSL');
editorService.set('root', parseDSL(value), { historySource: 'root-code' });
} catch (e: any) {
console.error(e);
}
};
</script>