mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
fix(embed): stabilize yys editor layout sizing in modal
This commit is contained in:
@@ -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'">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
|
<div v-if="showToolbar" ref="toolbarHostRef" class="toolbar-host">
|
||||||
<Toolbar
|
<Toolbar
|
||||||
v-if="showToolbar"
|
|
||||||
: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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
queueCanvasResize();
|
||||||
|
});
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
containerResizeObserver = new ResizeObserver(() => {
|
||||||
resizeCanvas();
|
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%;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user