fix(embed): stabilize yys editor layout sizing in modal

This commit is contained in:
2026-02-25 23:29:18 +08:00
parent 55376651bf
commit aa4554943c
2 changed files with 190 additions and 24 deletions

View File

@@ -1,5 +1,6 @@
<template> <template>
<div <div
ref="embedRootRef"
class="yys-editor-embed" class="yys-editor-embed"
:class="{ 'preview-mode': mode === 'preview', 'edit-mode': mode === 'edit' }" :class="{ 'preview-mode': mode === 'preview', 'edit-mode': mode === 'edit' }"
:style="containerStyle" :style="containerStyle"
@@ -7,16 +8,17 @@
<!-- 编辑模式完整 UI --> <!-- 编辑模式完整 UI -->
<template v-if="mode === 'edit'"> <template v-if="mode === 'edit'">
<!-- 工具栏 --> <!-- 工具栏 -->
<Toolbar <div v-if="showToolbar" ref="toolbarHostRef" class="toolbar-host">
v-if="showToolbar" <Toolbar
:is-embed="true" :is-embed="true"
:pinia-instance="localPinia" :pinia-instance="localPinia"
@save="handleSave" @save="handleSave"
@cancel="handleCancel" @cancel="handleCancel"
/> />
</div>
<!-- 主内容区 --> <!-- 主内容区 -->
<div class="editor-content"> <div class="editor-content" :style="editorContentStyle">
<!-- 左侧组件库 --> <!-- 左侧组件库 -->
<ComponentsPanel v-if="showComponentPanel" /> <ComponentsPanel v-if="showComponentPanel" />
@@ -24,7 +26,8 @@
<FlowEditor <FlowEditor
class="flow-editor-pane" class="flow-editor-pane"
ref="flowEditorRef" ref="flowEditorRef"
height="100%" :height="editorContentHeight"
:enable-label="false"
/> />
</div> </div>
@@ -85,6 +88,58 @@ export interface EdgeData {
properties?: Record<string, any> properties?: Record<string, any>
} }
const isPlainObject = (input: unknown): input is Record<string, any> => (
!!input && typeof input === 'object' && !Array.isArray(input)
)
const sanitizeLabelProperty = (properties: unknown): Record<string, any> | undefined => {
if (!isPlainObject(properties)) {
return undefined
}
const nextProperties: Record<string, any> = { ...properties }
if (Array.isArray(nextProperties._label)) {
const normalizedLabels = nextProperties._label.filter((label: any) => (
isPlainObject(label) && (label.id != null || label.text != null || label.value != null || label.content != null)
))
if (normalizedLabels.length === 0) {
delete nextProperties._label
} else {
nextProperties._label = normalizedLabels
}
}
return nextProperties
}
const sanitizeGraphData = (input?: GraphData | null): GraphData => {
if (!input || !Array.isArray(input.nodes) || !Array.isArray(input.edges)) {
return { nodes: [], edges: [] }
}
const nodes = input.nodes
.filter((node): node is NodeData => isPlainObject(node))
.map((node) => {
const nextNode: NodeData = { ...node }
const nextProperties = sanitizeLabelProperty(nextNode.properties)
if (nextProperties) {
nextNode.properties = nextProperties
}
return nextNode
})
const edges = input.edges
.filter((edge): edge is EdgeData => isPlainObject(edge))
.map((edge) => {
const nextEdge: EdgeData = { ...edge }
const nextProperties = sanitizeLabelProperty(nextEdge.properties)
if (nextProperties) {
nextEdge.properties = nextProperties
}
return nextEdge
})
return { nodes, edges }
}
export interface EditorConfig { export interface EditorConfig {
grid?: boolean grid?: boolean
snapline?: boolean snapline?: boolean
@@ -156,6 +211,10 @@ ensureElementPlusInstalled()
const flowEditorRef = ref<InstanceType<typeof FlowEditor>>() const flowEditorRef = ref<InstanceType<typeof FlowEditor>>()
const previewContainerRef = ref<HTMLElement | null>(null) const previewContainerRef = ref<HTMLElement | null>(null)
const previewLf = ref<LogicFlow | null>(null) const previewLf = ref<LogicFlow | null>(null)
const embedRootRef = ref<HTMLElement | null>(null)
const toolbarHostRef = ref<HTMLElement | null>(null)
let embedResizeObserver: ResizeObserver | null = null
const editorContentHeight = ref('100%')
// Computed // Computed
const effectiveCapability = computed<FlowCapabilityLevel>(() => { const effectiveCapability = computed<FlowCapabilityLevel>(() => {
@@ -174,12 +233,62 @@ const containerHeight = computed(() => {
return typeof props.height === 'number' ? `${props.height}px` : props.height return typeof props.height === 'number' ? `${props.height}px` : props.height
}) })
const editorContentStyle = computed(() => ({
height: editorContentHeight.value
}))
const recalcEditContentHeight = () => {
if (props.mode !== 'edit') {
return
}
const root = embedRootRef.value
if (!root) {
return
}
const rootHeight = root.clientHeight
const toolbarHeight = props.showToolbar ? (toolbarHostRef.value?.offsetHeight ?? 0) : 0
const contentHeight = Math.max(0, rootHeight - toolbarHeight)
if (contentHeight > 0) {
editorContentHeight.value = `${contentHeight}px`
} else {
editorContentHeight.value = '100%'
}
}
const triggerEditorResize = () => { const triggerEditorResize = () => {
nextTick(() => { nextTick(() => {
(flowEditorRef.value as any)?.resizeCanvas?.() recalcEditContentHeight()
const editor = flowEditorRef.value as any
editor?.resizeCanvas?.()
}) })
} }
const handleEmbedResize = () => {
if (props.mode === 'edit') {
recalcEditContentHeight()
triggerEditorResize()
return
}
if (props.mode === 'preview' && previewLf.value && previewContainerRef.value) {
const width = previewContainerRef.value.offsetWidth
const height = previewContainerRef.value.offsetHeight
previewLf.value.resize(width, height)
}
}
const setupEmbedResizeObserver = () => {
if (typeof ResizeObserver === 'undefined' || !embedRootRef.value) {
return
}
embedResizeObserver?.disconnect()
embedResizeObserver = new ResizeObserver(() => {
handleEmbedResize()
})
embedResizeObserver.observe(embedRootRef.value)
}
const destroyPreviewMode = () => { const destroyPreviewMode = () => {
if (previewLf.value) { if (previewLf.value) {
previewLf.value.destroy() previewLf.value.destroy()
@@ -217,7 +326,7 @@ const initPreviewMode = () => {
// 渲染数据 // 渲染数据
if (props.data) { if (props.data) {
previewLf.value.render(props.data) previewLf.value.render(sanitizeGraphData(props.data))
} }
} }
@@ -251,13 +360,14 @@ const getGraphData = (): GraphData | null => {
} }
const setGraphData = (data: GraphData) => { const setGraphData = (data: GraphData) => {
const safeData = sanitizeGraphData(data)
if (props.mode === 'edit') { if (props.mode === 'edit') {
const lfInstance = getLogicFlowInstance() const lfInstance = getLogicFlowInstance()
if (lfInstance) { if (lfInstance) {
lfInstance.render(data) lfInstance.render(safeData)
} }
} else if (props.mode === 'preview' && previewLf.value) { } else if (props.mode === 'preview' && previewLf.value) {
previewLf.value.render(data) previewLf.value.render(safeData)
} }
} }
@@ -283,8 +393,10 @@ watch(() => props.mode, (newMode) => {
}, 100) }, 100)
} else { } else {
destroyPreviewMode() destroyPreviewMode()
recalcEditContentHeight()
triggerEditorResize() triggerEditorResize()
} }
setupEmbedResizeObserver()
}) })
watch( watch(
@@ -303,6 +415,7 @@ watch(
[() => props.width, () => props.height, () => props.showToolbar, () => props.showComponentPanel], [() => props.width, () => props.height, () => props.showToolbar, () => props.showComponentPanel],
() => { () => {
if (props.mode === 'edit') { if (props.mode === 'edit') {
recalcEditContentHeight()
triggerEditorResize() triggerEditorResize()
} }
} }
@@ -310,9 +423,11 @@ watch(
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
setupEmbedResizeObserver()
if (props.mode === 'preview') { if (props.mode === 'preview') {
initPreviewMode() initPreviewMode()
} else if (props.mode === 'edit') { } else if (props.mode === 'edit') {
recalcEditContentHeight()
triggerEditorResize() triggerEditorResize()
// 编辑模式由 FlowEditor 组件初始化 // 编辑模式由 FlowEditor 组件初始化
// 等待 FlowEditor 初始化完成后加载数据 // 等待 FlowEditor 初始化完成后加载数据
@@ -320,6 +435,7 @@ onMounted(() => {
if (props.data) { if (props.data) {
setGraphData(props.data) setGraphData(props.data)
} }
recalcEditContentHeight()
triggerEditorResize() triggerEditorResize()
}, 500) }, 500)
} }
@@ -327,6 +443,8 @@ onMounted(() => {
// 清理 // 清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
embedResizeObserver?.disconnect()
embedResizeObserver = null
destroyPreviewMode() destroyPreviewMode()
destroyLogicFlowInstance() destroyLogicFlowInstance()
}) })
@@ -339,19 +457,27 @@ onBeforeUnmount(() => {
background: #f5f5f5; background: #f5f5f5;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
min-height: 0;
} }
.editor-content { .editor-content {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
height: 0;
overflow: hidden; overflow: hidden;
} }
.flow-editor-pane { .flow-editor-pane {
display: flex;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 100%; min-height: 0;
align-self: stretch;
}
.toolbar-host {
flex: 0 0 auto;
} }
.preview-mode { .preview-mode {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="editor-layout" :style="{ height }"> <div class="editor-layout" :style="{ height }">
<!-- 中间流程图区域 --> <!-- 中间流程图区域 -->
<div class="flow-container" :class="{ 'snapline-disabled': !snaplineEnabled }"> <div ref="flowHostRef" class="flow-container" :class="{ 'snapline-disabled': !snaplineEnabled }">
<div class="flow-controls"> <div class="flow-controls">
<div class="control-row toggles"> <div class="control-row toggles">
<label class="control-toggle"> <label class="control-toggle">
@@ -89,10 +89,14 @@ const MOVE_STEP = 2;
const MOVE_STEP_LARGE = 10; const MOVE_STEP_LARGE = 10;
const COPY_TRANSLATION = 40; const COPY_TRANSLATION = 40;
const props = defineProps<{ const props = withDefaults(defineProps<{
height?: string; height?: string;
}>(); enableLabel?: boolean;
}>(), {
enableLabel: true
});
const flowHostRef = ref<HTMLElement | null>(null);
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null);
const lf = ref<LogicFlow | null>(null); const lf = ref<LogicFlow | null>(null);
const selectedCount = ref(0); const selectedCount = ref(0);
@@ -115,17 +119,25 @@ const { showMessage } = useGlobalMessage();
const selectedNode = ref<any>(null); const selectedNode = ref<any>(null);
const copyBuffer = ref<GraphData | null>(null); const copyBuffer = ref<GraphData | null>(null);
let nextPasteDistance = COPY_TRANSLATION; let nextPasteDistance = COPY_TRANSLATION;
let containerResizeObserver: ResizeObserver | null = null;
const resolveResizeHost = () => {
const container = containerRef.value;
if (!container) return null;
return flowHostRef.value ?? (container.parentElement as HTMLElement | null) ?? container;
};
const resizeCanvas = () => { const resizeCanvas = () => {
const lfInstance = lf.value as any; const lfInstance = lf.value as any;
const container = containerRef.value; const resizeHost = resolveResizeHost();
if (!lfInstance || !container || typeof lfInstance.resize !== 'function') { if (!lfInstance || !resizeHost || typeof lfInstance.resize !== 'function') {
return; return;
} }
const width = container.clientWidth; const width = resizeHost.clientWidth;
const height = container.clientHeight; const height = resizeHost.clientHeight;
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
lfInstance.resize(width, height); lfInstance.resize(width, height);
return;
} }
}; };
@@ -149,6 +161,13 @@ function shouldSkipShortcut(event?: KeyboardEvent) {
return false; return false;
} }
const queueCanvasResize = () => {
resizeCanvas();
if (typeof window === 'undefined') return;
window.requestAnimationFrame(() => resizeCanvas());
setTimeout(() => resizeCanvas(), 120);
};
function ensureMeta(meta?: Record<string, any>) { function ensureMeta(meta?: Record<string, any>) {
const next: Record<string, any> = meta ? { ...meta } : {}; const next: Record<string, any> = meta ? { ...meta } : {};
if (next.visible == null) next.visible = true; if (next.visible == null) next.visible = true;
@@ -719,7 +738,14 @@ onMounted(() => {
fontSize: 14 fontSize: 14
} }
}, },
plugins: [Menu, Label, Snapshot, SelectionSelect, MiniMap, Control], plugins: [
Menu,
...(props.enableLabel ? [Label] : []),
Snapshot,
SelectionSelect,
MiniMap,
Control
],
pluginsOptions: { pluginsOptions: {
label: { label: {
isMultiple: false, // 每个节点只允许一个 label isMultiple: false, // 每个节点只允许一个 label
@@ -988,8 +1014,19 @@ onMounted(() => {
lfInstance.on('selection:drop', () => updateSelectedCount()); lfInstance.on('selection:drop', () => updateSelectedCount());
nextTick(() => { nextTick(() => {
resizeCanvas(); queueCanvasResize();
}); });
if (typeof ResizeObserver !== 'undefined') {
containerResizeObserver = new ResizeObserver(() => {
resizeCanvas();
});
if (flowHostRef.value) {
containerResizeObserver.observe(flowHostRef.value);
}
if (containerRef.value && containerRef.value !== flowHostRef.value) {
containerResizeObserver.observe(containerRef.value);
}
}
window.addEventListener('resize', handleWindowResize); window.addEventListener('resize', handleWindowResize);
}); });
@@ -1025,6 +1062,8 @@ defineExpose({
// 销毁 LogicFlow // 销毁 LogicFlow
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', handleWindowResize); window.removeEventListener('resize', handleWindowResize);
containerResizeObserver?.disconnect();
containerResizeObserver = null;
lf.value?.destroy(); lf.value?.destroy();
lf.value = null; lf.value = null;
destroyLogicFlowInstance(); destroyLogicFlowInstance();
@@ -1046,12 +1085,13 @@ onBeforeUnmount(() => {
.flow-container { .flow-container {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
min-height: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.container { .container {
width: 100%; width: 100%;
min-height: 300px; min-height: 0;
background: #fff; background: #fff;
height: 100%; height: 100%;
} }