Parcourir la source

feat: 完善删除功能;节点、边线工具

jiaxing.liao il y a 2 semaines
Parent
commit
beddc84019

+ 26 - 0
apps/web/src/views/Editor.vue

@@ -57,6 +57,8 @@
 					@run="handleRunWorkflow"
 					@update:nodes:position="handleUpdateNodesPosition"
 					@update:node:attrs="handleUpdateNodeProps"
+					@delete:node="handleDeleteNode"
+					@delete:connection="handleDeleteEdge"
 					class="bg-#f5f5f5"
 				>
 					<Toolbar @create:node="handleNodeCreate" />
@@ -314,6 +316,30 @@ const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
 	}
 }
 
+/**
+ * 删除节点
+ */
+const handleDeleteNode = (id: string) => {
+	console.log('del node', id)
+	const index = workflow.value.nodes.findIndex((node) => node.id === id)
+	if (index != -1) {
+		workflow.value.nodes.splice(index, 1)
+	}
+}
+
+/**
+ * 删除连线
+ */
+const handleDeleteEdge = (connection: Connection) => {
+	console.log('del edge', connection)
+	const index = workflow.value.edges.findIndex(
+		(edge) => edge.id === `edge-${connection.source}-${connection.target}`
+	)
+	if (index != -1) {
+		workflow.value.edges.splice(index, 1)
+	}
+}
+
 onBeforeUnmount(() => {
 	layout?.setMainStyle({})
 })

+ 5 - 0
packages/workflow/src/components/Canvas.vue

@@ -280,6 +280,10 @@ function onClickConnectionAdd(connection: Connection) {
 	emit('click:connection:add', connection)
 }
 
+function onDeleteNode(id: string) {
+	emit('delete:node', id)
+}
+
 let loaded = false
 function onNodesInitialized() {
 	if (!loaded) {
@@ -340,6 +344,7 @@ defineExpose({
 					:hovered="nodesHoveredById[nodeProps.id]"
 					@move="onUpdateNodePosition"
 					@update="onUpdateNodeAttrs"
+					@delete="onDeleteNode"
 				/>
 			</slot>
 		</template>

+ 31 - 5
packages/workflow/src/components/elements/edges/CanvasEdge.vue

@@ -1,12 +1,23 @@
 <script setup lang="ts">
 import { ref, watch, computed } from 'vue'
-import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from '@vue-flow/core'
-import { Icon } from '@repo/ui'
+import {
+	BaseEdge,
+	EdgeLabelRenderer,
+	getBezierPath,
+	type EdgeProps,
+	type Connection
+} from '@vue-flow/core'
+import { IconButton } from '@repo/ui'
 
 defineOptions({
 	inheritAttrs: false
 })
 
+const emit = defineEmits<{
+	delete: [connection: Connection]
+	add: [connection: Connection]
+}>()
+
 type CanvasEdgeProps = EdgeProps & {
 	readOnly?: boolean
 	hovered?: boolean
@@ -20,6 +31,13 @@ const delayedHovered = ref(props.hovered)
 const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
 const delayedHoveredTimeout = 600
 
+const connection = computed<Connection>(() => ({
+	source: props.source,
+	target: props.target,
+	sourceHandle: props.sourceHandleId,
+	targetHandle: props.targetHandleId
+}))
+
 watch(
 	() => props.hovered,
 	(isHovered) => {
@@ -36,6 +54,14 @@ watch(
 )
 
 const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
+
+const onAdd = () => {
+	emit('add', connection.value)
+}
+
+const onDelete = () => {
+	emit('delete', connection.value)
+}
 </script>
 
 <template>
@@ -50,9 +76,9 @@ const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
 			}"
 			class="nodrag nopan"
 		>
-			<div v-if="renderToolbar" class="flex gap-4px">
-				<Icon icon="lucide:plus" width="14" height="14" color="#6e6f6f" />
-				<Icon icon="lucide:brush-cleaning" width="14" height="14" color="#6e6f6f" />
+			<div v-if="renderToolbar" class="flex">
+				<IconButton icon="lucide:plus" size="small" square @click="onAdd" />
+				<IconButton icon="lucide:brush-cleaning" size="small" square @click="onDelete" />
 			</div>
 		</div>
 	</EdgeLabelRenderer>

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

@@ -5,6 +5,7 @@ import { nodeMap } from '@repo/nodes'
 
 import CanvasHandle from '../handles/CanvasHandle.vue'
 import NodeRenderer from './render-types/NodeRenderer.vue'
+import CanvasNodeToolBar from './CanvasNodeToolBar.vue'
 
 import type { NodeProps } from '@vue-flow/core'
 import type {
@@ -23,6 +24,7 @@ const props = defineProps<Props>()
 const emit = defineEmits<{
 	update: [id: string, parameters: Record<string, unknown>]
 	move: [id: string, position: { x: number; y: number }]
+	delete: [id: string]
 }>()
 
 /**
@@ -88,6 +90,10 @@ const onUpdate = (prop: Record<string, unknown>) => {
 	emit('update', props.id, prop)
 }
 
+const onDelete = () => {
+	emit('delete', props.id)
+}
+
 provide('canvas-node-data', {
 	props,
 	inputs,
@@ -106,5 +112,7 @@ provide('canvas-node-data', {
 		<template v-for="source in outputs" :key="'handle-outputs-port' + source.index">
 			<CanvasHandle v-bind="source" type="source" />
 		</template>
+
+		<CanvasNodeToolBar @delete="onDelete" />
 	</div>
 </template>

+ 50 - 7
packages/workflow/src/components/elements/node-tool-bar/index.vue

@@ -1,6 +1,24 @@
 <script setup lang="ts">
+import { ref, watch, inject, computed, type Ref } from 'vue'
+
 import { Icon } from '@repo/ui'
-import { ref } from 'vue'
+
+import type { IWorkflowNode, CanvasConnectionPort } from '../../../Interface'
+import type { NodeProps } from '@vue-flow/core'
+
+const emit = defineEmits<{
+	delete: []
+}>()
+
+const node = inject<{
+	props?: NodeProps<IWorkflowNode['data']> & {
+		readOnly?: boolean
+		hovered?: boolean
+	}
+	inputs?: Ref<CanvasConnectionPort[]>
+	outputs?: Ref<CanvasConnectionPort[]>
+}>('canvas-node-data')
+
 const barState = ref(false)
 const more = () => {
 	barState.value = !barState.value
@@ -11,10 +29,35 @@ const BarHandleClick = (state: string) => {
 	if (state === 'node-edit') {
 	}
 }
+
+const onDelete = () => {
+	emit('delete')
+}
+
+const delayedHovered = ref()
+const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
+const delayedHoveredTimeout = 600
+
+watch(
+	() => node?.props?.hovered,
+	(isHovered) => {
+		if (isHovered) {
+			if (delayedHoveredSetTimeoutRef.value) clearTimeout(delayedHoveredSetTimeoutRef.value)
+			delayedHovered.value = true
+		} else {
+			delayedHoveredSetTimeoutRef.value = setTimeout(() => {
+				delayedHovered.value = false
+			}, delayedHoveredTimeout)
+		}
+	},
+	{ immediate: true }
+)
+
+const renderToolbar = computed(() => delayedHovered.value && !node?.props?.readOnly)
 </script>
 
 <template>
-	<div class="node-tools relative">
+	<div class="node-tools absolute top--32px right-0" v-if="renderToolbar">
 		<div class="bar flex items-center">
 			<Icon
 				icon="lucide:play"
@@ -36,19 +79,19 @@ const BarHandleClick = (state: string) => {
 			v-show="barState"
 		>
 			<ul class="text-sm">
-				<li @click="BarHandleClick('node-run')">
+				<li @click.stop="BarHandleClick('node-run')">
 					<p>运行此步骤</p>
 				</li>
-				<li @click="BarHandleClick('node-edit')">
+				<li @click.stop="BarHandleClick('node-edit')">
 					<p>更改节点</p>
 				</li>
-				<li @click="BarHandleClick('node-copy')">
+				<li @click.stop="BarHandleClick('node-copy')">
 					<p>拷贝</p>
 				</li>
-				<li @click="BarHandleClick('node-delete')">
+				<li @click.stop="onDelete">
 					<p>删除</p>
 				</li>
-				<li @click="BarHandleClick('node-doc')">
+				<li @click.stop="BarHandleClick('node-doc')">
 					<p>查看文档</p>
 				</li>
 			</ul>

+ 38 - 66
packages/workflow/src/components/elements/nodes/render-types/NodeDefault.vue

@@ -1,52 +1,7 @@
-<template>
-	<div
-		:class="nodeClass"
-		class="default-node w-96px h-96px bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-8px relative"
-	>
-		<div className="w-full h-full relative flex items-center justify-center">
-			<div
-				class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg"
-				:style="{ background: nodeType?.iconColor }"
-			>
-				<div
-					class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
-				></div>
-				<Icon
-					:icon="nodeType?.icon ?? 'lucide:cloud'"
-					color="#ffffff"
-					class="relative z-10"
-					:size="20"
-				/>
-			</div>
-		</div>
-
-		<!-- toolbar -->
-		<div class="tool-bar absolute -top-9 right-0 h-7 pb-1 transition-all duration-300 ease-out">
-			<NodeToolbar v-if="delayedHovered" />
-		</div>
-
-		<!-- warning -->
-		<el-tooltip>
-			<template #content>{{ warningInfo || '请检查配置' }}</template>
-			<div v-if="warningInfo" class="absolute right-10px bottom-0">
-				<Icon icon="clarity:warning-solid" color="#ff4d4f" size="16" />
-			</div>
-		</el-tooltip>
-
-		<div className="absolute w-full bottom--24px text-12px text-center text-#333">
-			<div>{{ data?.title || nodeType?.displayName || '节点标题' }}</div>
-			<div className="text-12px text-center text-#999 truncate">
-				{{ data?.subtitle }}
-			</div>
-		</div>
-	</div>
-</template>
-
 <script setup lang="ts">
-import { ref, inject, computed, watch, type Ref } from 'vue'
+import { inject, computed, type Ref } from 'vue'
 import { Icon } from '@repo/ui'
 import { nodeMap } from '@repo/nodes'
-import NodeToolbar from '../../node-tool-bar/index.vue'
 
 import type { NodeProps } from '@vue-flow/core'
 import type { IWorkflowNode, CanvasConnectionPort } from '../../../../Interface'
@@ -83,26 +38,43 @@ const warningInfo = computed(() => {
 	const validate = nodeType.value?.validate
 	return validate && validate(data.value?.data)
 })
+</script>
 
-const delayedHovered = ref()
-const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
-const delayedHoveredTimeout = 600
+<template>
+	<div
+		:class="nodeClass"
+		class="default-node w-96px h-96px bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-8px relative"
+	>
+		<div className="w-full h-full relative flex items-center justify-center">
+			<div
+				class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg"
+				:style="{ background: nodeType?.iconColor }"
+			>
+				<div
+					class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+				></div>
+				<Icon
+					:icon="nodeType?.icon ?? 'lucide:cloud'"
+					color="#ffffff"
+					class="relative z-10"
+					:size="20"
+				/>
+			</div>
+		</div>
 
-watch(
-	() => node?.props?.hovered,
-	(isHovered) => {
-		console.log('isHovered', isHovered)
-		if (isHovered) {
-			if (delayedHoveredSetTimeoutRef.value) clearTimeout(delayedHoveredSetTimeoutRef.value)
-			delayedHovered.value = true
-		} else {
-			delayedHoveredSetTimeoutRef.value = setTimeout(() => {
-				delayedHovered.value = false
-			}, delayedHoveredTimeout)
-		}
-	},
-	{ immediate: true }
-)
+		<!-- warning -->
+		<el-tooltip>
+			<template #content>{{ warningInfo || '请检查配置' }}</template>
+			<div v-if="warningInfo" class="absolute right-10px bottom-0">
+				<Icon icon="clarity:warning-solid" color="#ff4d4f" size="16" />
+			</div>
+		</el-tooltip>
 
-const renderToolbar = computed(() => delayedHovered.value && !node?.props?.readOnly)
-</script>
+		<div className="absolute w-full bottom--24px text-12px text-center text-#333">
+			<div>{{ data?.title || nodeType?.displayName || '节点标题' }}</div>
+			<div className="text-12px text-center text-#999 truncate">
+				{{ data?.subtitle }}
+			</div>
+		</div>
+	</div>
+</template>

+ 5 - 5
packages/workflow/src/components/elements/nodes/render-types/NodeRenderer.vue

@@ -1,8 +1,3 @@
-<template>
-	<NodeStickyNote v-if="nodeType === 'stickyNote'" v-bind="$attrs" />
-	<NodeDefault v-else v-bind="$attrs"> </NodeDefault>
-</template>
-
 <script setup lang="ts">
 import { inject, computed, type Ref } from 'vue'
 import NodeDefault from './NodeDefault.vue'
@@ -19,3 +14,8 @@ const node = inject<{
 
 const nodeType = computed(() => node?.props?.data?.nodeType)
 </script>
+
+<template>
+	<NodeStickyNote v-if="nodeType === 'stickyNote'" v-bind="$attrs" />
+	<NodeDefault v-else v-bind="$attrs"> </NodeDefault>
+</template>