mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat(editor): 画布自适应大小
This commit is contained in:
parent
3c7d756d19
commit
ab3e113904
@ -55,7 +55,7 @@ import eventsService from '@editor/services/events';
|
||||
import historyService from '@editor/services/history';
|
||||
import propsService from '@editor/services/props';
|
||||
import uiService from '@editor/services/ui';
|
||||
import type { ComponentGroup, MenuBarData, Services, SideBarData } from '@editor/type';
|
||||
import type { ComponentGroup, MenuBarData, Services, SideBarData, StageRect } from '@editor/type';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'm-editor',
|
||||
@ -133,8 +133,8 @@ export default defineComponent({
|
||||
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
||||
},
|
||||
|
||||
stageStyle: {
|
||||
type: [String, Object] as PropType<Record<string, string | number>>,
|
||||
stageRect: {
|
||||
type: [String, Object] as PropType<StageRect>,
|
||||
},
|
||||
},
|
||||
|
||||
@ -208,8 +208,8 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.stageStyle,
|
||||
(stageStyle) => uiService.set('stageStyle', stageStyle),
|
||||
() => props.stageRect,
|
||||
(stageRect) => stageRect && uiService.set('stageRect', stageRect),
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="m-editor-stage">
|
||||
<div class="m-editor-stage" ref="stageWrap">
|
||||
<div
|
||||
class="m-editor-stage-container"
|
||||
ref="stageContainer"
|
||||
@ -17,6 +17,7 @@ import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
@ -31,7 +32,7 @@ import type { MApp, MNode, MPage } from '@tmagic/schema';
|
||||
import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage';
|
||||
import StageCore from '@tmagic/stage';
|
||||
|
||||
import type { Services } from '@editor/type';
|
||||
import type { Services, StageRect } from '@editor/type';
|
||||
|
||||
import ViewerMenu from './ViewerMenu.vue';
|
||||
|
||||
@ -88,26 +89,6 @@ export default defineComponent({
|
||||
|
||||
runtimeUrl: String,
|
||||
|
||||
root: {
|
||||
type: Object as PropType<MApp>,
|
||||
},
|
||||
|
||||
page: {
|
||||
type: Object as PropType<MPage>,
|
||||
},
|
||||
|
||||
node: {
|
||||
type: Object as PropType<MNode>,
|
||||
},
|
||||
|
||||
uiSelectMode: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
zoom: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
canSelect: {
|
||||
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
||||
default: (el: HTMLElement) => Boolean(el.id),
|
||||
@ -125,11 +106,20 @@ export default defineComponent({
|
||||
|
||||
setup(props, { emit }) {
|
||||
const services = inject<Services>('services');
|
||||
|
||||
const stageWrap = ref<HTMLDivElement>();
|
||||
const stageContainer = ref<HTMLDivElement>();
|
||||
|
||||
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 node = computed(() => services?.editorService.get<MNode>('node'));
|
||||
const stageStyle = computed(() => ({
|
||||
...services?.uiService.get<Record<string, string | number>>('stageStyle'),
|
||||
transform: `scale(${props.zoom}) translate3d(0, -50%, 0)`,
|
||||
width: `${stageRect.value?.width}px`,
|
||||
height: `${stageRect.value?.height}px`,
|
||||
transform: `scale(${zoom.value})`,
|
||||
}));
|
||||
|
||||
let stage: StageCore | null = null;
|
||||
@ -139,16 +129,16 @@ export default defineComponent({
|
||||
if (stage) return;
|
||||
|
||||
if (!stageContainer.value) return;
|
||||
if (!(props.runtimeUrl || props.render) || !props.root) return;
|
||||
if (!(props.runtimeUrl || props.render) || !root.value) return;
|
||||
|
||||
stage = new StageCore({
|
||||
render: props.render,
|
||||
runtimeUrl: props.runtimeUrl,
|
||||
zoom: props.zoom,
|
||||
zoom: zoom.value,
|
||||
canSelect: (el, stop) => {
|
||||
const elCanSelect = props.canSelect(el);
|
||||
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
|
||||
if (props.uiSelectMode && elCanSelect) {
|
||||
if (uiSelectMode.value && elCanSelect) {
|
||||
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
|
||||
return stop();
|
||||
}
|
||||
@ -176,45 +166,63 @@ export default defineComponent({
|
||||
services?.uiService.set('showGuides', true);
|
||||
});
|
||||
|
||||
if (!props.node?.id) return;
|
||||
if (!node.value?.id) return;
|
||||
stage?.on('runtime-ready', (rt) => {
|
||||
runtime = rt;
|
||||
// toRaw返回的值是一个引用而非快照,需要cloneDeep
|
||||
props.root && runtime?.updateRootConfig(cloneDeep(toRaw(props.root)));
|
||||
props.page?.id && runtime?.updatePageId?.(props.page.id);
|
||||
root.value && runtime?.updateRootConfig(cloneDeep(toRaw(root.value)));
|
||||
page.value?.id && runtime?.updatePageId?.(page.value.id);
|
||||
setTimeout(() => {
|
||||
props.node && stage?.select(toRaw(props.node.id));
|
||||
node.value && stage?.select(toRaw(node.value.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
watch(zoom, (zoom) => {
|
||||
if (!stage || !zoom) return;
|
||||
stage.setZoom(zoom);
|
||||
});
|
||||
|
||||
watch(root, (root) => {
|
||||
if (runtime && root) {
|
||||
runtime.updateRootConfig(cloneDeep(toRaw(root)));
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.zoom,
|
||||
(zoom) => {
|
||||
if (!stage || !zoom) return;
|
||||
stage?.setZoom(zoom);
|
||||
() => node.value?.id,
|
||||
(id) => {
|
||||
nextTick(() => {
|
||||
// 等待相关dom变更完成后,再select,适用大多数场景
|
||||
id && stage?.select(id);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.root,
|
||||
(root) => {
|
||||
if (runtime && root) {
|
||||
runtime.updateRootConfig(cloneDeep(toRaw(root)));
|
||||
}
|
||||
},
|
||||
);
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const { contentRect } of entries) {
|
||||
services?.uiService.set('stageContainerRect', {
|
||||
width: contentRect.width,
|
||||
height: contentRect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
stageWrap.value && resizeObserver.observe(stageWrap.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stage?.destroy();
|
||||
resizeObserver.disconnect();
|
||||
services?.editorService.set('stage', null);
|
||||
});
|
||||
|
||||
return {
|
||||
stageWrap,
|
||||
stageContainer,
|
||||
stageStyle,
|
||||
...useMenu(),
|
||||
|
||||
stageContainer,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -4,11 +4,6 @@
|
||||
:key="page?.id"
|
||||
:runtime-url="runtimeUrl"
|
||||
:render="render"
|
||||
:ui-select-mode="uiSelectMode"
|
||||
:root="root"
|
||||
:page="page"
|
||||
:node="node"
|
||||
:zoom="zoom"
|
||||
:moveable-options="moveableOptions"
|
||||
:can-select="canSelect"
|
||||
@select="selectHandler"
|
||||
@ -26,9 +21,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, nextTick, PropType, watch } from 'vue';
|
||||
import { computed, defineComponent, inject, PropType } from 'vue';
|
||||
|
||||
import type { MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema';
|
||||
import type { MComponent, MContainer, MPage } from '@tmagic/schema';
|
||||
import type { MoveableOptions, SortEventData } from '@tmagic/stage';
|
||||
import StageCore from '@tmagic/stage';
|
||||
|
||||
@ -63,23 +58,9 @@ export default defineComponent({
|
||||
|
||||
setup() {
|
||||
const services = inject<Services>('services');
|
||||
const node = computed(() => services?.editorService.get<MNode>('node'));
|
||||
const stage = computed(() => services?.editorService.get<StageCore>('stage'));
|
||||
|
||||
watch([() => node.value?.id, stage], ([id, stage]) => {
|
||||
nextTick(() => {
|
||||
// 等待相关dom变更完成后,再select,适用大多数场景
|
||||
id && stage?.select(id);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
uiSelectMode: computed(() => services?.uiService.get<boolean>('uiSelectMode')),
|
||||
root: computed(() => services?.editorService.get<MApp>('root')),
|
||||
page: computed(() => services?.editorService.get<MPage>('page')),
|
||||
zoom: computed(() => services?.uiService.get<number>('zoom')),
|
||||
|
||||
node,
|
||||
|
||||
selectHandler(el: HTMLElement) {
|
||||
services?.editorService.select(el.id);
|
||||
|
@ -21,7 +21,7 @@ import { reactive, toRaw } from 'vue';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import { GetColumnWidth, SetColumnWidth, UiState } from '@editor/type';
|
||||
import type { GetColumnWidth, SetColumnWidth, StageRect, UiState } from '@editor/type';
|
||||
|
||||
import BaseService from './BaseService';
|
||||
|
||||
@ -29,7 +29,14 @@ const state = reactive<UiState>({
|
||||
uiSelectMode: false,
|
||||
showSrc: false,
|
||||
zoom: 1,
|
||||
stageStyle: {},
|
||||
stageContainerRect: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
stageRect: {
|
||||
width: 375,
|
||||
height: 817,
|
||||
},
|
||||
columnWidth: {
|
||||
left: 310,
|
||||
center: globalThis.document.body.clientWidth - 310 - 400,
|
||||
@ -57,6 +64,11 @@ class Ui extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'stageRect') {
|
||||
this.setStageRect(value as unknown as StageRect);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'showGuides') {
|
||||
mask?.showGuides(value as unknown as boolean);
|
||||
}
|
||||
@ -66,6 +78,10 @@ class Ui extends BaseService {
|
||||
}
|
||||
|
||||
(state as any)[name] = value;
|
||||
|
||||
if (name === 'stageContainerRect') {
|
||||
state.zoom = this.calcZoom();
|
||||
}
|
||||
}
|
||||
|
||||
public get<T>(name: keyof typeof state): T {
|
||||
@ -94,6 +110,24 @@ class Ui extends BaseService {
|
||||
|
||||
state.columnWidth = columnWidth;
|
||||
}
|
||||
|
||||
private setStageRect(value: StageRect) {
|
||||
state.stageRect = {
|
||||
...state.stageRect,
|
||||
...value,
|
||||
};
|
||||
state.zoom = this.calcZoom();
|
||||
}
|
||||
|
||||
private calcZoom() {
|
||||
const { stageRect, stageContainerRect } = state;
|
||||
const { height, width } = stageContainerRect;
|
||||
if (!width || !height) return 1;
|
||||
if (width > stageRect.width && height > stageRect.height) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min((width - 100) / stageRect.width || 1, (height - 100) / stageRect.height || 1);
|
||||
}
|
||||
}
|
||||
|
||||
export type UiService = Ui;
|
||||
|
@ -2,18 +2,16 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - $--page-bar-height);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.m-editor-stage-container {
|
||||
transition: transform 0.3s;
|
||||
transform-origin: center -50%;
|
||||
z-index: 0;
|
||||
top: 50%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 375px;
|
||||
height: 80%;
|
||||
position: absolute;
|
||||
border: 1px solid $--border-color;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
@ -16,11 +16,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { FormConfig } from '@tmagic/form';
|
||||
import { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
||||
import StageCore from '@tmagic/stage';
|
||||
import type { FormConfig } from '@tmagic/form';
|
||||
import type { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
|
||||
import type { ComponentListService } from '@editor/services/componentList';
|
||||
import type { EditorService } from '@editor/services/editor';
|
||||
@ -75,6 +75,11 @@ export interface GetColumnWidth {
|
||||
right: number;
|
||||
}
|
||||
|
||||
export interface StageRect {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface UiState {
|
||||
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
|
||||
uiSelectMode: boolean;
|
||||
@ -82,8 +87,10 @@ export interface UiState {
|
||||
showSrc: boolean;
|
||||
/** 画布显示放大倍数,默认为 1 */
|
||||
zoom: number;
|
||||
/** 画布顶层div的样式,可用于改变画布的大小 */
|
||||
stageStyle: Record<string, string | number>;
|
||||
/** 画布容器的宽高 */
|
||||
stageContainerRect: StageRect;
|
||||
/** 画布顶层div的宽高,可用于改变画布的大小 */
|
||||
stageRect: StageRect;
|
||||
/** 编辑器列布局每一列的宽度,分为左中右三列 */
|
||||
columnWidth: GetColumnWidth;
|
||||
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import Editor from '@editor/layouts/CodeEditor.vue';
|
||||
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
describe('编辑器', () => {
|
||||
it('初始化', () => {
|
||||
const wrapper = mount(Editor, {
|
||||
props: {
|
||||
initValues: [
|
||||
{
|
||||
type: 'app',
|
||||
id: 1,
|
||||
name: 'app',
|
||||
items: [
|
||||
{
|
||||
type: 'page',
|
||||
id: 2,
|
||||
name: 'page',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
@ -20,6 +20,14 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
import Stage from '@editor/layouts/workspace/Stage.vue';
|
||||
|
||||
globalThis.ResizeObserver =
|
||||
globalThis.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
disconnect: jest.fn(),
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Stage.vue', () => {
|
||||
(global as any).fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
|
Loading…
x
Reference in New Issue
Block a user