lj1559651600@163.com před 1 týdnem
rodič
revize
4f8426a77d

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

@@ -73,6 +73,76 @@ const workflow = ref<IWorkflow>({
         //     }
         // }
     ]
+    // 	id: '1',
+    // 	nodes: [
+    // 		{
+    // 			id: 'node-1',
+    // 			type: 'canvas-node',
+    // 			position: { x: 100, y: 100 },
+    // 			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: 96,
+    // 			height: 96,
+    // 			position: { x: 400, y: 100 },
+    // 			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: [
+    // 		{
+    // 			id: 'edge-1-2',
+    // 			source: 'node-1',
+    // 			target: 'node-2',
+    // 			type: 'canvas-edge',
+    // 			data: {
+    // 				label: 'Edge 1-2'
+    // 			}
+    // 		}
+    // 	]
 })
 const nodeID = ref('')
 const setterVisible = ref(false)

+ 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'
+}

+ 5 - 2
packages/nodes/index.ts

@@ -2,8 +2,11 @@
  * @Author: liuJie
  * @Date: 2026-01-24 19:21:42
  * @LastEditors: liuJie
- * @LastEditTime: 2026-01-25 21:12:44
+ * @LastEditTime: 2026-01-26 09:27:16
  * @Describe: file describe
  */
 
-export *  from './materials';
+export *  from './materials';
+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']
+	}
+
+	// 其他方法和属性
+}

+ 3 - 2
packages/workflow/src/Interface.ts

@@ -1,5 +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
@@ -36,3 +35,5 @@ export interface XYPosition {
 	x: number
 	y: number
 }
+
+export { Position }

+ 77 - 7
packages/workflow/src/components/elements/CanvasNode.vue

@@ -1,27 +1,97 @@
 <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>
     <div 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 }}
+        <div className="absolute w-full bottom--22px text-12px text-center text-#222">
+            {{ data?.displayName }}
         </div>
-        <div className="absolute w-full bottom--38px text-12px text-center text-[#999] truncate">
+        <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>