Ver código fonte

feat: 添加节点模型,调整port

jiaxing.liao 1 semana atrás
pai
commit
0b7bc6a432

+ 46 - 6
apps/web/src/views/Editor.vue

@@ -15,17 +15,57 @@ const workflow = ref<IWorkflow>({
 			id: 'node-1',
 			type: 'canvas-node',
 			position: { x: 100, y: 100 },
-			width: 100,
-			height: 100,
-			data: { label: 'Node 1', inputs: [], outputs: [] }
+			width: 96,
+			height: 96,
+			data: {
+				version: ['1.0.0'],
+				displayName: '用户输入',
+				name: 'chart',
+				description: '通过用户输入开启流程处理',
+				icon: 'fluent:comment-multiple-28-regular',
+				iconColor: '#296dff',
+				inputs: [],
+				outputs: [
+					{
+						index: 0,
+						type: 'main'
+					}
+				]
+			}
 		},
 		{
 			id: 'node-2',
 			type: 'canvas-node',
-			width: 100,
-			height: 100,
+			width: 96,
+			height: 96,
 			position: { x: 400, y: 100 },
-			data: { label: 'Node 1', inputs: [], outputs: [] }
+			data: {
+				version: ['1.0.0'],
+				displayName: '条件判断',
+				name: 'if',
+				description: '通过条件判断拆分多个流程分支',
+				icon: 'roentgen:guidepost',
+				iconColor: '#108e49',
+				inputs: [
+					{
+						index: 0,
+						type: 'main'
+					}
+				],
+				outputs: [
+					{
+						index: 0,
+						type: 'main',
+						label: 'true'
+					},
+					{
+						index: 1,
+						type: 'main',
+						label: 'false'
+					}
+				],
+				outputNames: ['true', 'false']
+			}
 		}
 	],
 	edges: [

+ 63 - 0
packages/nodes/Interface.ts

@@ -0,0 +1,63 @@
+export interface INodeData {
+	/**
+	 * 版本信息
+	 */
+	version: string[]
+	/**
+	 * 展示名称
+	 */
+	displayName: string
+	/**
+	 * 名称
+	 */
+	name: string
+	/**
+	 * 副标题 带格式化信息
+	 */
+	subtitle?: string
+	/**
+	 * 描述
+	 */
+	description?: string
+	/**
+	 * 节点图标 默认使用iconify图标名称
+	 * @example 'mdi:home'
+	 * 参考 https://icon-sets.iconify.design/
+	 */
+	icon?: string
+	/**
+	 * 图标颜色
+	 */
+	iconColor?: string
+	/**
+	 * 输入端口列表
+	 */
+	inputs: string[]
+	/**
+	 * 输入端口名称
+	 */
+	inputNames?: string[]
+	/**
+	 * 输出端口列表
+	 */
+	outputs: string[]
+	/**
+	 * 输出端口名称
+	 */
+	outputNames?: string[]
+}
+
+export interface INodeType {
+	/**
+	 * 节点数据定义
+	 */
+	schema: INodeData
+	// todo 扩展节点行为方法
+}
+
+/**
+ * 节点连接类型
+ */
+export const NodeConnectionTypes = {
+	main: 'main'
+}

+ 3 - 0
packages/nodes/index.ts

@@ -0,0 +1,3 @@
+export * from './nodes/chat/chat.node'
+
+export * from './Interface'

+ 17 - 0
packages/nodes/nodes/chat/chat.node.ts

@@ -0,0 +1,17 @@
+import type { INodeData, INodeType } from '../../Interface'
+import { NodeConnectionTypes } from '../../Interface'
+
+export class Chat implements INodeType {
+	schema: INodeData = {
+		version: ['1.0.0'],
+		displayName: '用户输入',
+		name: 'chart',
+		description: '通过用户输入开启流程处理',
+		icon: 'fluent:comment-multiple-28-regular',
+		iconColor: '#296dff',
+		inputs: [],
+		outputs: [NodeConnectionTypes.main]
+	}
+
+	// 其他方法和属性
+}

+ 18 - 0
packages/nodes/nodes/if/if.node.ts

@@ -0,0 +1,18 @@
+import type { INodeData, INodeType } from '../../Interface'
+import { NodeConnectionTypes } from '../../Interface'
+
+export class Chat implements INodeType {
+	schema: INodeData = {
+		version: ['1.0.0'],
+		displayName: '条件判断',
+		name: 'if',
+		description: '通过条件判断拆分多个流程分支',
+		icon: 'roentgen:guidepost',
+		iconColor: '#108e49',
+		inputs: [NodeConnectionTypes.main],
+		outputs: [NodeConnectionTypes.main, NodeConnectionTypes.main],
+		outputNames: ['true', 'false']
+	}
+
+	// 其他方法和属性
+}

+ 11 - 1
packages/workflow/src/Interface.ts

@@ -1,4 +1,4 @@
-import type { Node, DefaultEdge } from '@vue-flow/core'
+import { type Node, type DefaultEdge, Position } from '@vue-flow/core'
 
 export type CanvasConnectionPort = {
 	node?: string
@@ -9,6 +9,14 @@ export type CanvasConnectionPort = {
 	label?: string
 }
 
+export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
+	handleId: string
+	connectionsCount: number
+	isConnecting: boolean
+	position: Position
+	offset?: { top?: string; left?: string }
+}
+
 export interface IWorkflowNode extends Node {
 	data: {
 		inputs: CanvasConnectionPort[]
@@ -34,3 +42,5 @@ export interface XYPosition {
 	x: number
 	y: number
 }
+
+export { Position }

+ 75 - 5
packages/workflow/src/components/elements/CanvasNode.vue

@@ -1,11 +1,76 @@
 <script setup lang="ts">
+import { computed } from 'vue'
 import { Position } from '@vue-flow/core'
 import type { NodeProps } from '@vue-flow/core'
+import type {
+	IWorkflowNode,
+	CanvasConnectionPort,
+	CanvasElementPortWithRenderData
+} from '../../Interface'
 
 import { Icon } from '@repo/ui'
 import CanvasHandle from './handles/CanvasHandle.vue'
 
-const props = defineProps<NodeProps>()
+type Props = NodeProps<IWorkflowNode['data']> & {
+	readOnly?: boolean
+	hovered?: boolean
+}
+
+const props = defineProps<Props>()
+
+/**
+ * 处理节点
+ */
+const createEndpoint = (data: {
+	port: CanvasConnectionPort
+	index: number
+	count: number
+	offsetAxis: 'top' | 'left'
+	position: Position
+}): CanvasElementPortWithRenderData => {
+	const { port, index, count, offsetAxis, position } = data
+
+	return {
+		...port,
+		handleId: `${port.type}-${index}`,
+		position,
+		connectionsCount: count,
+		isConnecting: false,
+		offset: {
+			[offsetAxis]: `${(100 / (count + 1)) * (index + 1)}%`
+		}
+	}
+}
+
+/**
+ * Inputs
+ */
+const inputs = computed(() =>
+	(props.data.inputs || []).map((target, index) =>
+		createEndpoint({
+			port: target,
+			index,
+			count: props.data.inputs.length,
+			offsetAxis: 'top',
+			position: Position.Left
+		})
+	)
+)
+
+/**
+ * Outputs
+ */
+const outputs = computed(() =>
+	(props.data.outputs || []).map((source, index) =>
+		createEndpoint({
+			port: source,
+			index,
+			count: props.data.outputs.length,
+			offsetAxis: 'top',
+			position: Position.Right
+		})
+	)
+)
 </script>
 
 <template>
@@ -13,17 +78,22 @@ const props = defineProps<NodeProps>()
 		class="w-full h-full bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-4px relative"
 	>
 		<div className="w-full h-full relative flex items-center justify-center">
-			<Icon icon="lucide:zoom-out" height="40" width="40" color="#00bb88" />
+			<Icon :icon="data?.icon" height="40" width="40" :color="data?.iconColor" />
 		</div>
 
 		<div className="absolute w-full bottom--22px text-12px text-center text-#222">
-			{{ data.label }}
+			{{ data?.displayName }}
 		</div>
 		<div className="absolute w-full bottom--38px text-12px text-center text-#999 truncate">
 			{{ data.subtitle }}
 		</div>
 
-		<CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
-		<CanvasHandle handle-id="2" handle-type="target" :position="Position.Left" />
+		<template v-for="target in inputs" :key="'handle-inputs-port' + target.index">
+			<CanvasHandle v-bind="target" type="target" />
+		</template>
+
+		<template v-for="source in outputs" :key="'handle-outputs-port' + source.index">
+			<CanvasHandle v-bind="source" type="source" />
+		</template>
 	</div>
 </template>

+ 4 - 25
packages/workflow/src/components/elements/handles/CanvasHandle.vue

@@ -5,12 +5,13 @@ import HandlePort from './HandlePort.vue'
 defineProps<{
 	handleId: string
 	handleClasses?: string | string[]
-	handleType: 'source' | 'target'
 	position: Position
 	offset?: Record<string, string>
 	isConnectableStart?: boolean
 	isConnectableEnd?: boolean
 	isValidConnection?: ValidConnectionFunc
+	type: 'source' | 'target'
+	label?: string
 }>()
 </script>
 
@@ -19,14 +20,14 @@ defineProps<{
 		v-bind="$attrs"
 		:id="handleId"
 		:class="$style.handle"
-		:type="handleType"
+		:type="type"
 		:position="position"
 		:style="offset"
 		:connectable-start="isConnectableStart"
 		:connectable-end="isConnectableEnd"
 		:is-valid-connection="isValidConnection"
 	>
-		<HandlePort :position="position" :type="handleType" />
+		<HandlePort :position="position" :type="type" :label="label" />
 	</Handle>
 </template>
 
@@ -51,26 +52,4 @@ defineProps<{
 		cursor: default;
 	}
 }
-
-.renderType {
-	&.top {
-		margin-bottom: -16px;
-		transform: translate(0%, -50%);
-	}
-
-	&.right {
-		margin-left: -16px;
-		transform: translate(50%, 0%);
-	}
-
-	&.left {
-		margin-right: -16px;
-		transform: translate(-50%, 0%);
-	}
-
-	&.bottom {
-		margin-top: -16px;
-		transform: translate(0%, 50%);
-	}
-}
 </style>

+ 30 - 4
packages/workflow/src/components/elements/handles/HandlePort.vue

@@ -1,11 +1,15 @@
 <template>
-	<div :class="[position, type]" class="handlePort transition-transform duration-150"></div>
+	<div :class="[position, type]" class="renderType flex items-center">
+		<div :class="type" class="handlePort transition-transform duration-150 relative"></div>
+		<div v-if="label" class="ml-8px text-12px text-#666">{{ label }}</div>
+	</div>
 </template>
 
 <script setup lang="ts">
 defineProps<{
 	position: string
 	type: 'source' | 'target'
+	label?: string
 }>()
 </script>
 
@@ -19,9 +23,31 @@ defineProps<{
 	cursor: default;
 }
 
-.handlePort.source:hover {
-	transform: scale(1.5);
-	border-width: 1.5px;
+.source:hover {
+	transform: scale(1.2);
+	border-width: 1.2px;
 	cursor: crosshair;
 }
+
+.renderType {
+	&.top {
+		margin-bottom: -12px;
+		transform: translate(0%, -50%);
+	}
+
+	&.right {
+		margin-left: -12px;
+		transform: translate(50%, 0%);
+	}
+
+	&.left {
+		margin-right: -12px;
+		transform: translate(-50%, 0%);
+	}
+
+	&.bottom {
+		margin-top: -12px;
+		transform: translate(0%, 50%);
+	}
+}
 </style>