Kaynağa Gözat

pref: 优化节点弹窗位置

jiaxing.liao 1 ay önce
ebeveyn
işleme
5aba33c69d

+ 74 - 6
apps/web/src/views/editor/NodeView.vue

@@ -48,12 +48,13 @@
 		v-model:visible="setterVisible"
 	/>
 	<el-popover
+		v-if="libaryRefferenceRef"
 		:visible="showNodeLibary"
-		width="360px"
-		virtual-triggering
 		:show-arrow="false"
 		:append-to="workflowWrapperRef"
 		:virtual-ref="libaryRefferenceRef"
+		width="360px"
+		virtual-triggering
 	>
 		<NodeLibary @add-node="handleNodeCreate" @mouseleave="onHideNodeLibary" />
 	</el-popover>
@@ -111,6 +112,7 @@ const workflowWrapperRef = ref<HTMLElement>()
 const workflowRef = ref<InstanceType<typeof Workflow>>()
 const showNodeLibary = ref(false)
 const libaryRefferenceRef = ref<HTMLElement>()
+const nodeLibaryPopoverAnchorRef = ref<HTMLElement>()
 const runVisible = ref(false)
 const closeRunWorkflowOnSubmit = ref(false)
 const runWorkflowInputOnly = ref(false)
@@ -120,6 +122,7 @@ const setterVisible = ref(false)
 const nodeID = ref('')
 const setterActiveTab = ref<'setting' | 'last-run'>('setting')
 const displayNodeExecutionStatus = ref<Record<string, CanvasExecutionStatus>>({})
+const shouldAutoCenterAfterInit = ref(true)
 const peddingHandlePayload = ref<{
 	by?: 'node' | 'edge'
 	handle?: ConnectStartEvent
@@ -132,6 +135,31 @@ const peddingHandlePayload = ref<{
 const runningStatusStartedAt = new Map<string, number>()
 const pendingNodeStatusTimers = new Map<string, number>()
 
+const removeNodeLibaryPopoverAnchor = () => {
+	nodeLibaryPopoverAnchorRef.value?.remove()
+	nodeLibaryPopoverAnchorRef.value = undefined
+}
+
+const createNodeLibaryPopoverAnchor = (event?: MouseEvent) => {
+	removeNodeLibaryPopoverAnchor()
+
+	if (!event) {
+		return undefined
+	}
+
+	const anchor = document.createElement('div')
+	anchor.style.position = 'fixed'
+	anchor.style.left = `${event.clientX}px`
+	anchor.style.top = `${event.clientY}px`
+	anchor.style.width = '1px'
+	anchor.style.height = '1px'
+	anchor.style.pointerEvents = 'none'
+	document.body.appendChild(anchor)
+	nodeLibaryPopoverAnchorRef.value = anchor
+
+	return anchor
+}
+
 // 在为节点安排新的状态切换前,先清理旧的延时任务。
 const clearPendingNodeStatusTimer = (nodeId: string) => {
 	const timer = pendingNodeStatusTimers.get(nodeId)
@@ -336,6 +364,27 @@ const openSetter = (id: string, tab: 'setting' | 'last-run' = 'setting') => {
 	setterVisible.value = true
 }
 
+const centerWorkflowIntoView = async () => {
+	const vueFlow = workflowRef.value?.getVueFlow()
+	if (!vueFlow || !props.workflow?.nodes?.length) {
+		return false
+	}
+
+	await nextTick()
+
+	await new Promise<void>((resolve) => {
+		window.requestAnimationFrame(() => resolve())
+	})
+
+	await vueFlow.fitView({
+		padding: 0.2,
+		duration: 240,
+		maxZoom: 1
+	})
+
+	return true
+}
+
 const openRunWorkflow = async (
 	nodeId: string,
 	initialTab: 'input' | 'trigger' | 'result' | 'detail' | 'trace',
@@ -442,6 +491,21 @@ const handleRunAgent = (id?: string) => {
 	handleRunNode(targetNode.id)
 }
 
+watch(
+	() => props.workflow?.nodes,
+	async (nodes) => {
+		if (!shouldAutoCenterAfterInit.value || !nodes?.length) {
+			return
+		}
+
+		const centered = await centerWorkflowIntoView()
+		if (centered) {
+			shouldAutoCenterAfterInit.value = false
+		}
+	},
+	{ flush: 'post' }
+)
+
 /**
  * 创建新节点
  */
@@ -736,17 +800,20 @@ const onConnectionOpenNodeLibary = async (payload: {
 	parentId?: string
 }) => {
 	await nextTick()
-	const { handle } = payload
-	libaryRefferenceRef.value = handle.event?.target as HTMLElement
+	libaryRefferenceRef.value = createNodeLibaryPopoverAnchor(payload.event)
 	showNodeLibary.value = true
 	peddingHandlePayload.value = payload
 }
 
 // 关闭节点库弹层时,清理相关的临时状态。
 const onHideNodeLibary = () => {
-	peddingHandlePayload.value = undefined
-	libaryRefferenceRef.value = undefined
 	showNodeLibary.value = false
+
+	setTimeout(() => {
+		peddingHandlePayload.value = undefined
+		libaryRefferenceRef.value = undefined
+		removeNodeLibaryPopoverAnchor()
+	}, 500)
 }
 
 // 根据边上加号按钮的位置打开节点库弹层。
@@ -773,6 +840,7 @@ const handleClickConectionAdd = (connection: Connection & { id: string }, parent
 }
 
 onBeforeUnmount(() => {
+	removeNodeLibaryPopoverAnchor()
 	resetDisplayedNodeStatuses()
 })
 </script>

+ 3 - 2
packages/workflow/src/components/Canvas.vue

@@ -410,8 +410,9 @@ let loaded = false
 async function onNodesInitialized() {
 	if (!loaded) {
 		if (props.zoomToFit) {
-			onZoomToFit()
-			onResetZoom()
+			await fitView({
+				maxZoom: 1
+			})
 		} else {
 			await setViewport(defaultViewport)
 		}

+ 6 - 8
packages/workflow/src/components/elements/nodes/CanvasNode.vue

@@ -105,7 +105,8 @@ const getConnectionCount = (
  */
 const inputs = computed(() => {
 	const getInputs = nodeMap.value?.[props.node?.data.nodeType]?.inputs
-	const inputs = typeof getInputs === 'function' ? getInputs(props.node?.data) : getInputs || []
+	const inputs =
+		typeof getInputs === 'function' ? getInputs(props.node?.data) : getInputs || ['main']
 
 	return (inputs as CanvasConnectionPort[]).map((target, index) =>
 		createEndpoint({
@@ -129,7 +130,8 @@ const inputs = computed(() => {
  */
 const outputs = computed(() => {
 	const getOutputs = nodeMap.value?.[props.node?.data.nodeType]?.outputs
-	const outputs = typeof getOutputs === 'function' ? getOutputs(props.node?.data) : getOutputs || []
+	const outputs =
+		typeof getOutputs === 'function' ? getOutputs(props.node?.data) : getOutputs || ['main']
 
 	return (outputs as CanvasConnectionPort[]).map((target, index) =>
 		createEndpoint({
@@ -183,16 +185,12 @@ provideCanvasNodeContext({
 			@add-inner-edge="emit('add-inner-edge', $event)"
 			@create:connection:cancelled="emit('create-connection-cancelled', $event)"
 			@loop:child:click:node="(id, position) => emit('loop:child:click:node', id, position)"
-			@loop:child:dblclick:node="
-				(id, position) => emit('loop:child:dblclick:node', id, position)
-			"
+			@loop:child:dblclick:node="(id, position) => emit('loop:child:dblclick:node', id, position)"
 			@loop:child:click:node:add="emit('loop:child:click:node:add', $event)"
 			@loop:child:create:connection:end="
 				(connection, event) => emit('loop:child:create:connection:end', connection, event)
 			"
-			@loop:child:update:node:attrs="
-				(id, attrs) => emit('loop:child:update:node:attrs', id, attrs)
-			"
+			@loop:child:update:node:attrs="(id, attrs) => emit('loop:child:update:node:attrs', id, attrs)"
 			@loop:child:update:nodes:position="emit('loop:child:update:nodes:position', $event)"
 			@loop:child:create:connection:cancelled="
 				emit('loop:child:create:connection:cancelled', $event)