فهرست منبع

Merge branch 'main' of https://git.shalu.com/Shalu/shalu-agent-workflow

jiaxing.liao 3 هفته پیش
والد
کامیت
e22d020edc
34فایلهای تغییر یافته به همراه2021 افزوده شده و 308 حذف شده
  1. 1 1
      apps/web/package.json
  2. 48 0
      apps/web/src/components/RunWork.vue
  3. 46 0
      apps/web/src/components/setter/CodeSetter.vue
  4. 46 0
      apps/web/src/components/setter/ConditionSetter.vue
  5. 46 0
      apps/web/src/components/setter/DatabaseSetter.vue
  6. 46 0
      apps/web/src/components/setter/HttpSetter.vue
  7. 27 0
      apps/web/src/components/setter/index.vue
  8. 224 70
      apps/web/src/views/Editor.vue
  9. 1 1
      packages/nodes/Interface.ts
  10. 9 0
      packages/nodes/index.ts
  11. 21 0
      packages/nodes/materials/code.ts
  12. 20 0
      packages/nodes/materials/condition.ts
  13. 20 0
      packages/nodes/materials/database.ts
  14. 22 0
      packages/nodes/materials/end.ts
  15. 21 0
      packages/nodes/materials/http.ts
  16. 26 0
      packages/nodes/materials/index.ts
  17. 22 0
      packages/nodes/materials/start.ts
  18. 74 0
      packages/nodes/materials/toolbar.ts
  19. 18 7
      packages/nodes/package.json
  20. 1 0
      packages/workflow/package.json
  21. 1 1
      packages/workflow/src/Interface.ts
  22. 118 98
      packages/workflow/src/components/Canvas.vue
  23. 41 34
      packages/workflow/src/components/elements/CanvasControlBar.vue
  24. 56 58
      packages/workflow/src/components/elements/CanvasNode.vue
  25. 71 0
      packages/workflow/src/components/elements/handles/AddNode.vue
  26. 25 4
      packages/workflow/src/components/elements/handles/CanvasHandle.vue
  27. 4 30
      packages/workflow/src/components/elements/handles/HandlePort.vue
  28. 239 0
      packages/workflow/src/components/elements/node-temp/CodeNode.vue
  29. 173 0
      packages/workflow/src/components/elements/node-temp/ConditionNode.vue
  30. 243 0
      packages/workflow/src/components/elements/node-temp/DataBaseNode.vue
  31. 75 0
      packages/workflow/src/components/elements/node-temp/EndNode.vue
  32. 125 0
      packages/workflow/src/components/elements/node-temp/HttpNode1.vue
  33. 76 0
      packages/workflow/src/components/elements/node-temp/StartNode.vue
  34. 35 4
      pnpm-lock.yaml

+ 1 - 1
apps/web/package.json

@@ -9,6 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@repo/nodes": "workspace:^",
     "element-plus": "^2.13.1",
     "normalize.css": "^8.0.1",
     "vue": "^3.5.24",
@@ -16,7 +17,6 @@
   },
   "devDependencies": {
     "@repo/workflow": "workspace:*",
-    "@types/node": "^24.10.1",
     "@vitejs/plugin-vue": "^6.0.1",
     "@vue/tsconfig": "^0.8.1",
     "less": "^4.5.1",

+ 48 - 0
apps/web/src/components/RunWork.vue

@@ -0,0 +1,48 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-24 21:25:04
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-24 22:01:08
+ * @Describe: 运行工作流
+-->
+<script lang="ts" setup>
+import { ElDrawer, ElButton } from 'element-plus';
+import { Icon } from '@iconify/vue';
+const props = withDefaults(
+    defineProps<{
+        visible: boolean,
+    }>(),
+    {
+        visible: false,
+    }
+);
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='content'>
+        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+
+            <template #header>
+                <h4>运行工作流</h4>
+                <Icon icon="lucide:x" height="24" width="24"></Icon>
+            </template>
+
+            <!-- Drawer content -->
+            This is drawer content.
+
+            <template #footer>
+                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
+                    运行
+                </ElButton>
+            </template>
+
+        </ElDrawer>
+    </div>
+</template>
+<style lang="less" scoped>
+.pt-0 {
+    padding-top: 0px;
+}
+</style>

+ 46 - 0
apps/web/src/components/setter/CodeSetter.vue

@@ -0,0 +1,46 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 22:08:04
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 23:04:14
+ * @Describe: code设置器
+-->
+<script lang="ts" setup>
+import { ElDrawer, ElButton } from 'element-plus';
+import { Icon } from '@iconify/vue';
+const props = withDefaults(
+    defineProps<{
+        data: any,
+        visible: boolean,
+    }>(),
+    {
+        visible: false,
+        data: {}
+    }
+);
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='content'>
+        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+
+            <template #header>
+                <h4>代码执行</h4>
+                <Icon icon="lucide:x" height="24" width="24"></Icon>
+            </template>
+
+            <!-- Drawer content -->
+            This is drawer content.
+
+            <!-- <template #footer>
+                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
+                    运行
+                </ElButton>
+            </template> -->
+
+        </ElDrawer>
+    </div>
+</template>
+<style lang="scss" scoped></style>

+ 46 - 0
apps/web/src/components/setter/ConditionSetter.vue

@@ -0,0 +1,46 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 22:08:04
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 23:04:45
+ * @Describe: 条件设置器
+-->
+<script lang="ts" setup>
+import { ElDrawer, ElButton } from 'element-plus';
+import { Icon } from '@iconify/vue';
+const props = withDefaults(
+    defineProps<{
+        data: any,
+        visible: boolean,
+    }>(),
+    {
+        visible: false,
+        data: {}
+    }
+);
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='content'>
+        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+
+            <template #header>
+                <h4>条件</h4>
+                <Icon icon="lucide:x" height="24" width="24"></Icon>
+            </template>
+
+            <!-- Drawer content -->
+            This is drawer content.
+
+            <!-- <template #footer>
+                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
+                    运行
+                </ElButton>
+            </template> -->
+
+        </ElDrawer>
+    </div>
+</template>
+<style lang="scss" scoped></style>

+ 46 - 0
apps/web/src/components/setter/DatabaseSetter.vue

@@ -0,0 +1,46 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 22:08:04
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 23:05:13
+ * @Describe: 数据设置器
+-->
+<script lang="ts" setup>
+import { ElDrawer, ElButton } from 'element-plus';
+import { Icon } from '@iconify/vue';
+const props = withDefaults(
+    defineProps<{
+        data: any,
+        visible: boolean,
+    }>(),
+    {
+        visible: false,
+        data: {}
+    }
+);
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='content'>
+        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+
+            <template #header>
+                <h4>数据查询</h4>
+                <Icon icon="lucide:x" height="24" width="24"></Icon>
+            </template>
+
+            <!-- Drawer content -->
+            This is drawer content.
+
+            <!-- <template #footer>
+                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
+                    运行
+                </ElButton>
+            </template> -->
+
+        </ElDrawer>
+    </div>
+</template>
+<style lang="scss" scoped></style>

+ 46 - 0
apps/web/src/components/setter/HttpSetter.vue

@@ -0,0 +1,46 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 22:08:04
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:24:34
+ * @Describe: http设置器
+-->
+<script lang="ts" setup>
+import { ElDrawer, ElButton } from 'element-plus';
+import { Icon } from '@iconify/vue';
+const props = withDefaults(
+    defineProps<{
+        data: any,
+        visible: boolean,
+    }>(),
+    {
+        visible: false,
+        data: {}
+    }
+);
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='content'>
+        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+
+            <template #header>
+                <h4>HTTP请求</h4>
+                <Icon icon="lucide:x" height="24" width="24"></Icon>
+            </template>
+
+            <!-- Drawer content -->
+            This is drawer content.
+
+            <!-- <template #footer>
+                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
+                    运行
+                </ElButton>
+            </template> -->
+
+        </ElDrawer>
+    </div>
+</template>
+<style lang="scss" scoped></style>

+ 27 - 0
apps/web/src/components/setter/index.vue

@@ -0,0 +1,27 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 22:13:06
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:26:30
+ * @Describe: file describe
+-->
+<script lang="ts" setup>
+import HttpSetter from './HttpSetter.vue';
+interface Props {
+    data: any, // 暂时定义
+    visible: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+    id: '',
+    visible: false
+})
+const emit = defineEmits<{
+    'update:visible': [value: boolean]
+}>()
+</script>
+<template>
+    <div class='setter'>
+        <HttpSetter :data="data" v-model:visible="props.visible" />
+    </div>
+</template>
+<style lang="less" scoped></style>

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

@@ -1,91 +1,245 @@
 <template>
-	<div class="w-full h-full">
-		<Workflow :workflow="workflow" @click:node="handleNodeClick" @drop="handleDrop" />
-	</div>
+    <div class="w-full h-full">
+        <Workflow :workflow="workflow" @click:node="handleNodeClick" @create:node="handleNodeCreate" @drop="handleDrop"
+            @run="handleRunWorkflow" />
+        <RunWork v-model:visible="runVisible" />
+        <Setter :data="nodeID" v-model:visible="setterVisible" />
+    </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { startNode, endNode, httpNode, conditionNode, databaseNode, codeNode, } from '@repo/nodes'
 import { Workflow, type IWorkflow, type XYPosition } from '@repo/workflow'
+import Setter from "@/components/setter/index.vue"
+import RunWork from '@/components/RunWork.vue'
+import type { SourceType } from '@repo/nodes'
+import { ref } from 'vue'
 
 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: [
+    id: '1',
+    nodes: [
+        startNode, // 初始化节点,
+        endNode, // 初始化节点,
+        // httpNode,
+        // conditionNode,
+        // databaseNode,
+        // codeNode
 					{
-						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'
+						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'
+								}
+							]
+						}
 					},
 					{
-						index: 1,
-						type: 'main',
-						label: 'false'
+						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']
+						}
 					}
 				],
-				outputNames: ['true', 'false']
-			}
-		}
-	],
-	edges: [
-		{
-			id: 'edge-1-2',
-			source: 'node-1',
-			target: 'node-2',
-			type: 'canvas-edge',
-			data: {
-				label: 'Edge 1-2'
+    edges: [
+			{
+				id: 'edge-1-2',
+				source: 'node-1',
+				target: 'node-2',
+				type: 'canvas-edge',
+				data: {
+					label: 'Edge 1-2'
+				}
 			}
-		}
-	]
+        // {
+        //     id: 'edge-1-2',
+        //     source: 'start-node',
+        //     target: 'http-node',
+        //     type: 'canvas-edge',
+        //     data: {
+        //         label: 'Edge 1-2'
+        //     }
+        // },
+        // {
+        //     id: 'edge-1-6',
+        //     source: 'http-node',
+        //     target: 'condition-node',
+        //     type: 'canvas-edge',
+        //     data: {
+        //         label: 'Edge 1-2'
+        //     }
+        // },
+        // {
+        //     id: 'edge-1-5',
+        //     source: 'condition-node',
+        //     target: 'data-node',
+        //     type: 'canvas-edge',
+        //     data: {
+        //         label: 'Edge 1-2'
+        //     }
+        // },
+        // {
+        //     id: 'edge-1-3',
+        //     source: 'database-node',
+        //     target: 'code-node',
+        //     type: 'canvas-edge',
+        //     data: {
+        //         label: 'Edge 1-2'
+        //     }
+        // },
+
+        // {
+        //     id: 'edge-1-4',
+        //     source: 'code-node',
+        //     target: 'end-node',
+        //     type: 'canvas-edge',
+        //     data: {
+        //         label: 'Edge 1-2'
+        //     }
+        // }
+    ]
+    // 	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)
+const runVisible = ref(false)
+const handleRunWorkflow = () => {
+    runVisible.value = true
+    console.log('run workflow')
+}
+const handleNodeCreate = (value: SourceType) => {
+    console.log(value)
+
+    const nodeMap: Record<string, any> = {
+        'http': httpNode,
+        'condition': conditionNode,
+        'code': codeNode,
+        'database': databaseNode
+    }
+    const nodeToAdd = nodeMap[value.type]
 
+    // 如果存在对应节点则添加
+    if (nodeToAdd) {
+        workflow.value.nodes.push(nodeToAdd)
+    }
+    console.log(workflow.value.nodes, 'workflow.nodes')
+}
 const handleNodeClick = (id: string, position: XYPosition) => {
-	console.log('click node', id, position)
+    console.log('click node', id, position)
+    nodeID.value = id
+    setterVisible.value = true
 }
 
 const handleDrop = (position: XYPosition, event: DragEvent) => {
-	console.log('drag and drop at', position, event)
+    console.log('drag and drop at', position, event)
 }
 </script>

+ 1 - 1
packages/nodes/Interface.ts

@@ -52,7 +52,7 @@ export interface INodeType {
 	 * 节点数据定义
 	 */
 	schema: INodeData
-	// todo 扩展节点行为方法
+
 }
 
 /**

+ 9 - 0
packages/nodes/index.ts

@@ -1,3 +1,12 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:21:42
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-26 09:27:16
+ * @Describe: file describe
+ */
+
+export *  from './materials';
 export * from './nodes/chat/chat.node'
 
 export * from './Interface'

+ 21 - 0
packages/nodes/materials/code.ts

@@ -0,0 +1,21 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:48:30
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:02:44
+ * @Describe: code 节点
+ */
+import type { IWorkflowNode} from '@repo/workflow'
+
+export const codeNode: IWorkflowNode = {
+    id: 'code-node',
+    type: 'code-node',
+    label: '代码节点',
+    position:{x: 904, y: 378},
+    data: {
+        id: 'code-node-1',
+        description: '代码节点',
+        inputs: [],
+        outputs: []
+    }
+}

+ 20 - 0
packages/nodes/materials/condition.ts

@@ -0,0 +1,20 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:45:51
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:01:53
+ * @Describe: 条件节点
+ */
+import type { IWorkflowNode} from '@repo/workflow'
+export const conditionNode: IWorkflowNode = {
+    id: 'condition-node',
+    type: 'condition-node',
+    label: '条件判断',
+    position: {x: 605, y: 417},
+    data: {
+        id: 'condition-node-1',
+        description: '条件判断节点',
+        inputs: [],
+        outputs: []
+    }
+}

+ 20 - 0
packages/nodes/materials/database.ts

@@ -0,0 +1,20 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:51:27
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:03:17
+ * @Describe: 数据查询节点
+ */
+import type { IWorkflowNode} from '@repo/workflow'
+export const databaseNode: IWorkflowNode = {
+    id: 'database-node',
+    type: 'database-node',
+    label: '数据查询',
+    position: {x: 835, y: 518},
+    data: {
+        id: 'database-node-1',
+        description: '数据查询节点',
+        inputs: [],
+        outputs: []
+    }
+}

+ 22 - 0
packages/nodes/materials/end.ts

@@ -0,0 +1,22 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:44:57
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:00:26
+ * @Describe: 结束节点
+ */
+import type { IWorkflowNode } from '@repo/workflow'
+
+export const endNode: IWorkflowNode = {
+    id: 'end-node',
+    type: 'end-node',
+    label: '结束',
+    position: {x: 643.987980769231, y: 225.97019230769232},
+    data: {
+        id: 'end-node',
+        label: '结束节点',
+        description: '这是一个结束节点',
+        inputs: [],
+        outputs: []
+    }
+}

+ 21 - 0
packages/nodes/materials/http.ts

@@ -0,0 +1,21 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:45:07
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 22:00:48
+ * @Describe: HTTP请求节点
+ */
+import type { IWorkflowNode} from '@repo/workflow'
+
+export const httpNode: IWorkflowNode = {
+    id: 'http-node',
+    type: 'http-node',
+    label: 'http',
+    position: {x: 468, y: 370},
+    data: {
+        id: 'http-node-1',
+        description: 'http请求节点',
+        inputs: [],
+        outputs: []
+    }
+};

+ 26 - 0
packages/nodes/materials/index.ts

@@ -0,0 +1,26 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:23:49
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 21:53:48
+ * @Describe: 节点物料管理
+ */
+import { startNode } from './start'
+import { endNode } from './end'
+import { httpNode }from './http'
+import { conditionNode }from './condition'
+import { databaseNode }from './database'
+import { codeNode }from './code'
+
+import { materialTools, type SourceType} from './toolbar'
+
+export {
+    startNode,
+    endNode,
+    httpNode,
+    conditionNode,
+    databaseNode,
+    codeNode,
+    materialTools,
+    type SourceType,
+}

+ 22 - 0
packages/nodes/materials/start.ts

@@ -0,0 +1,22 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:26:16
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 21:21:04
+ * @Describe: 开始节点,
+ */
+import type { IWorkflowNode } from '@repo/workflow'
+
+export const startNode:IWorkflowNode = {
+    id: 'start-node',
+    type: 'start-node',
+    label: '开始',
+    position: { x: 257, y: 203 },
+    data: {
+        id: 'start-node',
+        label: '开始节点',
+        description: '这是一个开始节点',
+        inputs: [],
+        outputs: []
+    }
+}

+ 74 - 0
packages/nodes/materials/toolbar.ts

@@ -0,0 +1,74 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-25 00:06:41
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 21:52:47
+ * @Describe: 工具栏配置
+ */
+export interface MaterialToolType {
+    label: string;
+    id: string;
+    description: string;
+    source: Array<SourceType>;
+};
+
+export interface SourceType {
+    name: string;
+    type: string;
+    icon: string;
+    component: string;
+    id: string;
+    data?: any;
+    active: boolean;
+    isEdit: boolean;
+};
+
+
+export  const materialTools:MaterialToolType[] = [
+    {
+        label: '业务逻辑',
+        id: 'basic-nodes',
+        description: '业务节点',
+        source: [{
+                name: 'HTTP请求',
+                type: 'http',
+                icon: 'lucide:link',
+                component: 'Http',
+                id: 'http-node-id',
+                data: {},
+                active: false,
+                isEdit: false,
+            },
+            {
+                name: '条件分支',
+                type: 'condition',
+                icon: 'lucide:trending-up-down',
+                component: 'Condition',
+                id: 'condition-node-id',
+                data: {},
+                active: false,
+                isEdit: false,
+            },
+            {
+                name: '代码执行',
+                type: 'code',
+                icon: 'lucide:code',
+                component: 'Code',
+                id: 'code-node-id',
+                data: {},
+                active: false,
+                isEdit: false,
+            },
+            {
+                name: '数据查询',
+                type: 'database',
+                icon: 'lucide:database-zap',
+                component: 'Database',
+                id: 'data-query-node-id',
+                data: {},
+                active: false,
+                isEdit: false,
+            },
+        ],
+    }
+];

+ 18 - 7
packages/nodes/package.json

@@ -1,9 +1,20 @@
 {
-  "name": "@repo/nodes",
-  "version": "1.0.0",
-  "type": "module",
-  "private": true,
-  "exports": {
-    ".": "./index.ts"
-  }
+    "name": "@repo/nodes",
+    "version": "1.0.0",
+    "type": "module",
+    "private": true,
+    "exports": {
+        ".": "./index.ts"
+    },
+    "devDependencies": {
+        "@repo/nodes": "workspace:*",
+        "@repo/workflow": "workspace:*",
+        "@repo/typescript-config": "workspace:*",
+        "tailwindcss": "^4.1.18",
+        "vue": "^3.5.24"
+    },
+    "dependencies": {
+        "less": "^4.5.1",
+        "less-loader": "^12.3.0"
+    }
 }

+ 1 - 0
packages/workflow/package.json

@@ -28,6 +28,7 @@
   "devDependencies": {
     "@repo/typescript-config": "workspace:*",
     "@repo/ui": "workspace:*",
+    "@repo/nodes": "workspace:*",
     "@types/node": "^24.10.1",
     "@vitejs/plugin-vue": "^6.0.1",
     "@vue/tsconfig": "^0.8.1",

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

@@ -7,6 +7,7 @@ export type CanvasConnectionPort = {
 	required?: boolean
 	maxConnections?: number
 	label?: string
+    [key: string]: any
 }
 
 export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
@@ -16,7 +17,6 @@ export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
 	position: Position
 	offset?: { top?: string; left?: string }
 }
-
 export interface IWorkflowNode extends Node {
 	data: {
 		inputs: CanvasConnectionPort[]

+ 118 - 98
packages/workflow/src/components/Canvas.vue

@@ -2,60 +2,68 @@
 import { VueFlow, useVueFlow, type NodeMouseEvent, MarkerType } from '@vue-flow/core'
 import { MiniMap } from '@vue-flow/minimap'
 import type { IWorkflow, XYPosition } from '../Interface'
+import type { SourceType } from '@repo/nodes'
 
 import CanvasNode from './elements/CanvasNode.vue'
 import CanvasEdge from './elements/CanvasEdge.vue'
 import CanvasBackground from './elements/background/CanvasBackground.vue'
 import CanvasControlBar from './elements/CanvasControlBar.vue'
+import ConditionNode from './elements/node-temp/ConditionNode.vue'
+import StartNode from './elements/node-temp/StartNode.vue'
+import HttpNode from './elements/node-temp/HttpNode1.vue'
+import EndNode from './elements/node-temp/EndNode.vue'
+import CodeNode from './elements/node-temp/CodeNode.vue'
+import DataBaseNode from './elements/node-temp/DataBaseNode.vue'
 
 defineOptions({
-	name: 'workflow-canvas'
+    name: 'workflow-canvas'
 })
 
 const emit = defineEmits<{
-	'update:node:position': [id: string, position: XYPosition]
-	'update:node:activated': [id: string, event?: MouseEvent]
-	'update:node:deactivated': [id: string]
-	'update:node:enabled': [id: string]
-	'update:node:selected': [id?: string]
-	'update:node:name': [id: string]
-	'update:node:parameters': [id: string, parameters: Record<string, unknown>]
-	'update:node:inputs': [id: string]
-	'update:node:outputs': [id: string]
-	'update:logs-open': [open?: boolean]
-	'update:logs:input-open': [open?: boolean]
-	'update:logs:output-open': [open?: boolean]
-	'update:has-range-selection': [isActive: boolean]
-	'click:node': [id: string, position: XYPosition]
-	'click:node:add': [id: string, handle: string]
-	'run:node': [id: string]
-	'copy:production:url': [id: string]
-	'copy:test:url': [id: string]
-	'delete:node': [id: string]
-	'replace:node': [id: string]
-	'create:node': [source: any]
-	'create:sticky': []
-	'delete:nodes': [ids: string[]]
-	'update:nodes:enabled': [ids: string[]]
-	'copy:nodes': [ids: string[]]
-	'duplicate:nodes': [ids: string[]]
-	'cut:nodes': [ids: string[]]
-	'drag-and-drop': [position: XYPosition, event: DragEvent]
+    'update:node:position': [id: string, position: XYPosition]
+    'update:node:activated': [id: string, event?: MouseEvent]
+    'update:node:deactivated': [id: string]
+    'update:node:enabled': [id: string]
+    'update:node:selected': [id?: string]
+    'update:node:name': [id: string]
+    'update:node:parameters': [id: string, parameters: Record<string, unknown>]
+    'update:node:inputs': [id: string]
+    'update:node:outputs': [id: string]
+    'update:logs-open': [open?: boolean]
+    'update:logs:input-open': [open?: boolean]
+    'update:logs:output-open': [open?: boolean]
+    'update:has-range-selection': [isActive: boolean]
+    'click:node': [id: string, position: XYPosition]
+    'click:node:add': [id: string, handle: string]
+    'run:node': [id: string]
+    'copy:production:url': [id: string]
+    'copy:test:url': [id: string]
+    'delete:node': [id: string]
+    'replace:node': [id: string]
+    'create:node': [source: any]
+    'create:sticky': []
+    'delete:nodes': [ids: string[]]
+    'update:nodes:enabled': [ids: string[]]
+    'copy:nodes': [ids: string[]]
+    'duplicate:nodes': [ids: string[]]
+    'cut:nodes': [ids: string[]]
+    'drag-and-drop': [position: XYPosition, event: DragEvent]
+    'run': []
 }>()
 
 const props = withDefaults(
-	defineProps<{
-		id?: string
-		nodes: IWorkflow['nodes']
-		edges: IWorkflow['edges']
-		readOnly?: boolean
-	}>(),
-	{
-		id: 'canvas',
-		readOnly: false,
-		nodes: () => [],
-		edges: () => []
-	}
+    defineProps<{
+        id?: string
+        nodes: IWorkflow['nodes']
+        edges: IWorkflow['edges']
+        readOnly?: boolean
+    }>(),
+    {
+        id: 'canvas',
+        readOnly: false,
+        nodes: () => [],
+        edges: () => []
+    }
 )
 
 const vueFlow = useVueFlow(props.id)
@@ -66,91 +74,103 @@ const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vue
  * Returns the position of a mouse or touch event
  */
 const getMousePosition = (event: MouseEvent | TouchEvent): XYPosition => {
-	const x = (event && 'clientX' in event ? event.clientX : event?.touches?.[0]?.clientX) ?? 0
-	const y = (event && 'clientY' in event ? event.clientY : event?.touches?.[0]?.clientY) ?? 0
+    const x = (event && 'clientX' in event ? event.clientX : event?.touches?.[0]?.clientX) ?? 0
+    const y = (event && 'clientY' in event ? event.clientY : event?.touches?.[0]?.clientY) ?? 0
 
-	return { x, y }
+    return { x, y }
 }
 
 function getProjectedPosition(event?: MouseEvent | TouchEvent) {
-	const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
-	const { x, y } = event ? getMousePosition(event) : { x: 0, y: 0 }
+    const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
+    const { x, y } = event ? getMousePosition(event) : { x: 0, y: 0 }
 
-	return project({
-		x: x - bounds.left,
-		y: y - bounds.top
-	})
+    return project({
+        x: x - bounds.left,
+        y: y - bounds.top
+    })
 }
 
 const onNodeClick = ({ node, event }: NodeMouseEvent) => {
-	emit('click:node', node.id, getProjectedPosition(event))
+    emit('click:node', node.id, getProjectedPosition(event))
 }
 
 function onDrop(event: DragEvent) {
-	const position = getProjectedPosition(event)
+    const position = getProjectedPosition(event)
 
-	emit('drag-and-drop', position, event)
+    emit('drag-and-drop', position, event)
 }
 
 const onZoomIn = () => {
-	zoomIn()
+    zoomIn()
 }
 
 const onZoomOut = () => {
-	zoomOut()
+    zoomOut()
 }
 
 const onZoomToFit = () => {
-	fitView()
+    fitView()
 }
 
 const onResetZoom = () => {
-	zoomTo(1)
+    zoomTo(1)
 }
+const onAddNode = (value: SourceType) => {
+    emit('create:node', value)
+}
+const handleRun = () => {
+    emit('run')
+}
+console.log(props.nodes)
 </script>
 
 <template>
-	<VueFlow
-		:id="id"
-		:nodes="nodes"
-		:edges="edges"
-		:connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
-		:connection-radius="60"
-		@node-click="onNodeClick"
-		@drop="onDrop"
-		v-bind="$attrs"
-	>
-		<template #node-canvas-node="nodeProps">
-			<CanvasNode v-bind="nodeProps" />
-		</template>
-
-		<template #edge-canvas-edge="edgeProps">
-			<CanvasEdge v-bind="edgeProps" />
-		</template>
-
-		<template #background>
-			<rect width="100%" height="100%" fill="#f0f0f0" />
-		</template>
-
-		<MiniMap
-			:height="120"
-			:width="180"
-			:node-border-radius="16"
-			class="bg-white bottom-40px!"
-			position="bottom-left"
-			pannable
-			zoomable
-		/>
-
-		<CanvasControlBar
-			@zoom-in="onZoomIn"
-			@zoom-out="onZoomOut"
-			@zoom-to-fit="onZoomToFit"
-			@reset-zoom="onResetZoom"
-		/>
-
-		<CanvasBackground :viewport="viewport" :striped="readOnly" />
-	</VueFlow>
+    <VueFlow :id="id" :nodes="nodes" :edges="edges" :connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
+        :connection-radius="60" @node-click="onNodeClick" @drop="onDrop" v-bind="$attrs">
+        <template #node-canvas-node="nodeProps">
+            <CanvasNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-start-node="nodeProps">
+            <StartNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-end-node="nodeProps">
+            <EndNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-http-node="nodeProps">
+            <HttpNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-code-node="nodeProps">
+            <CodeNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-database-node="nodeProps">
+            <DataBaseNode v-bind="nodeProps" />
+        </template>
+
+        <template #node-condition-node="nodeProps">
+            <ConditionNode v-bind="nodeProps" />
+        </template>
+
+        <template #edge-canvas-edge="edgeProps">
+            <CanvasEdge v-bind="edgeProps" />
+        </template>
+
+        <template #background>
+            <rect width="100%" height="100%" fill="#f0f0f0" />
+        </template>
+
+        <MiniMap :height="120" :width="180" :node-border-radius="16" class="bg-white bottom-40px!"
+            position="bottom-left" pannable zoomable />
+
+        <CanvasControlBar @zoom-in="onZoomIn" @zoom-out="onZoomOut" @zoom-to-fit="onZoomToFit" @reset-zoom="onResetZoom"
+            @add-node="onAddNode" @run="handleRun" />
+
+        <CanvasBackground :viewport="viewport" :striped="readOnly" />
+    </VueFlow>
 </template>
 
 <style>

+ 41 - 34
packages/workflow/src/components/elements/CanvasControlBar.vue

@@ -2,61 +2,68 @@
 import { Controls } from '@vue-flow/controls'
 import { Icon } from '@iconify/vue'
 import { ElButton } from 'element-plus'
-
+import AddNode from './handles/AddNode.vue';
+import type { SourceType } from '@repo/nodes'
 const emit = defineEmits<{
-	'reset-zoom': []
-	'zoom-in': []
-	'zoom-out': []
-	'zoom-to-fit': []
-	'tidy-up': []
-	'toggle-zoom-mode': []
+    'reset-zoom': []
+    'zoom-in': []
+    'zoom-out': []
+    'zoom-to-fit': []
+    'tidy-up': []
+    'toggle-zoom-mode': []
+    'add-node': [value: SourceType]
+    'run': []
 }>()
 
 function onResetZoom() {
-	emit('reset-zoom')
+    emit('reset-zoom')
 }
 
 function onZoomIn() {
-	emit('zoom-in')
+    emit('zoom-in')
 }
 
 function onZoomOut() {
-	emit('zoom-out')
+    emit('zoom-out')
 }
 
 function onZoomToFit() {
-	emit('zoom-to-fit')
+    emit('zoom-to-fit')
 }
 
-// function onTidyUp() {
-// 	emit('tidy-up')
-// }
+function onAddNode(value: SourceType) {
+    emit('add-node', value)
+}
+function onRun() {
+    emit('run')
+}
 </script>
 
 <template>
-	<Controls :show-fit-view="false" :show-zoom="false" :show-interactive="false">
-		<div class="flex gap-0px">
-			<ElButton @click="onZoomToFit" square>
-				<Icon icon="lucide:maximize" height="16" width="16" />
-			</ElButton>
-			<ElButton @click="onZoomIn">
-				<Icon icon="lucide:zoom-in" height="16" width="16" />
-			</ElButton>
-			<ElButton @click="onZoomOut">
-				<Icon icon="lucide:zoom-out" height="16" width="16" />
-			</ElButton>
-			<ElButton @click="onResetZoom">
-				<Icon icon="lucide:undo-2" height="16" width="16" />
-			</ElButton>
-			<!-- <ElButton @click="onTidyUp">
-				<Icon icon="lucide:brush-cleaning" height="16" width="16" />
-			</ElButton> -->
-		</div>
-	</Controls>
+    <Controls :show-fit-view="false" :show-zoom="false" :show-interactive="false">
+        <div class="flex gap-0px">
+            <ElButton @click="onZoomToFit" square>
+                <Icon icon="lucide:maximize" height="16" width="16" />
+            </ElButton>
+            <ElButton @click="onZoomIn">
+                <Icon icon="lucide:zoom-in" height="16" width="16" />
+            </ElButton>
+            <ElButton @click="onZoomOut">
+                <Icon icon="lucide:zoom-out" height="16" width="16" />
+            </ElButton>
+            <ElButton @click="onResetZoom">
+                <Icon icon="lucide:undo-2" height="16" width="16" />
+            </ElButton>
+            <AddNode @add-node="onAddNode" />
+            <ElButton @click="onRun" type="success">
+                <Icon icon="lucide:play" height="16" width="16" class="mr-1" /> 执行
+            </ElButton>
+        </div>
+    </Controls>
 </template>
 
 <style lang="less">
 .el-button {
-	padding: 8px;
+    padding: 8px;
 }
 </style>

+ 56 - 58
packages/workflow/src/components/elements/CanvasNode.vue

@@ -3,17 +3,17 @@ import { computed } from 'vue'
 import { Position } from '@vue-flow/core'
 import type { NodeProps } from '@vue-flow/core'
 import type {
-	IWorkflowNode,
-	CanvasConnectionPort,
-	CanvasElementPortWithRenderData
+    IWorkflowNode,
+    CanvasConnectionPort,
+    CanvasElementPortWithRenderData
 } from '../../Interface'
 
 import { Icon } from '@repo/ui'
 import CanvasHandle from './handles/CanvasHandle.vue'
 
 type Props = NodeProps<IWorkflowNode['data']> & {
-	readOnly?: boolean
-	hovered?: boolean
+    readOnly?: boolean
+    hovered?: boolean
 }
 
 const props = defineProps<Props>()
@@ -22,78 +22,76 @@ const props = defineProps<Props>()
  * 处理节点
  */
 const createEndpoint = (data: {
-	port: CanvasConnectionPort
-	index: number
-	count: number
-	offsetAxis: 'top' | 'left'
-	position: Position
+    port: CanvasConnectionPort
+    index: number
+    count: number
+    offsetAxis: 'top' | 'left'
+    position: Position
 }): CanvasElementPortWithRenderData => {
-	const { port, index, count, offsetAxis, position } = data
+    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)}%`
-		}
-	}
+    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
-		})
-	)
+    (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
-		})
-	)
+    (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="data?.icon" height="40" width="40" :color="data?.iconColor" />
-		</div>
+    <div class="w-full h-full bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-4px relative">
+        <div class="w-full h-full relative flex items-center justify-center">
+            <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?.displayName }}
-		</div>
-		<div className="absolute w-full bottom--38px text-12px text-center text-#999 truncate">
-			{{ data.subtitle }}
-		</div>
+        <div class="absolute w-full bottom--22px text-12px text-center text-#222">
+            {{ data?.displayName }}
+        </div>
+        <div class="absolute w-full bottom--38px text-12px text-center text-#999 truncate">
+            {{ data.subtitle }}
+        </div>
 
-		<template v-for="target in inputs" :key="'handle-inputs-port' + target.index">
-			<CanvasHandle v-bind="target" type="target" />
-		</template>
+        <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 v-for="source in outputs" :key="'handle-outputs-port' + source.index">
+            <CanvasHandle v-bind="source" type="source" />
+        </template>
+    </div>
 </template>

+ 71 - 0
packages/workflow/src/components/elements/handles/AddNode.vue

@@ -0,0 +1,71 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-24 16:40:11
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 11:56:32
+ * @Describe: 添加物料
+-->
+<script lang="ts" setup>
+import { ref, reactive } from 'vue';
+import { Icon } from '@iconify/vue';
+import { ElPopover, ElButton } from 'element-plus';
+import { materialTools, type SourceType } from '@repo/nodes'
+
+const materials = reactive(materialTools)
+defineOptions({
+    name: 'AddMaterialsPop'
+})
+const emit = defineEmits<{
+    'add-node': [value: SourceType]
+}>()
+const onAddNode = (value: SourceType) => {
+    emit('add-node', value)
+    show.value = false
+}
+const show = ref(false);
+const togglePop = () => {
+    show.value = !show.value;
+}
+</script>
+<template>
+    <ElPopover v-bind:visible="show" trigger="click" transition="el-zoom-in-top" :show-after="400" :hide-after="1000"
+        placement="top">
+        <div v-for="item in materials" :key="item.id">
+            <p class="mb-2 mt-1 text-[#676f83]">{{ item.label }}</p>
+            <ul>
+                <li class="tool mb-3 flex items-center" v-for="value in item.source" :key="value.id"
+                    @click="onAddNode(value)">
+                    <Icon :icon="value.icon" height="16" width="16" class="mr-2 bg-[#6172f3] p-1 rounded"
+                        color="#fff" />
+                    <span>{{ value.name }}</span>
+                </li>
+            </ul>
+        </div>
+        <template #reference>
+            <ElButton @click="togglePop" type="primary">
+                <Icon icon="lucide:package-plus" height="16" width="16" class="mr-1" /> 新增节点
+            </ElButton>
+        </template>
+    </ElPopover>
+</template>
+<style lang="less" scoped>
+ul {
+    padding: 0 0 0 10px;
+
+    .tool {
+        list-style: none;
+        cursor: pointer;
+
+        span {
+            color: #354052;
+        }
+
+        &:hover {
+
+            span {
+                color: #6172f3;
+            }
+        }
+    }
+}
+</style>

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

@@ -5,13 +5,12 @@ 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>
 
@@ -20,14 +19,14 @@ defineProps<{
 		v-bind="$attrs"
 		:id="handleId"
 		:class="$style.handle"
-		:type="type"
+		:type="handleType"
 		:position="position"
 		:style="offset"
 		:connectable-start="isConnectableStart"
 		:connectable-end="isConnectableEnd"
 		:is-valid-connection="isValidConnection"
 	>
-		<HandlePort :position="position" :type="type" :label="label" />
+		<HandlePort :position="position" :type="handleType" />
 	</Handle>
 </template>
 
@@ -52,4 +51,26 @@ 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>

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

@@ -1,15 +1,11 @@
 <template>
-	<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>
+	<div :class="[position, type]" class="handlePort transition-transform duration-150"></div>
 </template>
 
 <script setup lang="ts">
 defineProps<{
 	position: string
 	type: 'source' | 'target'
-	label?: string
 }>()
 </script>
 
@@ -23,31 +19,9 @@ defineProps<{
 	cursor: default;
 }
 
-.source:hover {
-	transform: scale(1.2);
-	border-width: 1.2px;
+.handlePort.source:hover {
+	transform: scale(1.5);
+	border-width: 1.5px;
 	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>

+ 239 - 0
packages/workflow/src/components/elements/node-temp/CodeNode.vue

@@ -0,0 +1,239 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 16:56:55
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 19:10:12
+ * @Describe: 代码执行节点
+-->
+<script setup lang="ts">
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import { Icon } from '@repo/ui'
+import { computed } from 'vue'
+
+interface CodeConfig {
+    language: 'javascript' | 'python' | 'groovy' | 'java'
+    content: string
+    inputVars?: string[]
+    outputVar?: string
+}
+
+interface Environment {
+    timeout?: number
+    memory?: number
+}
+
+interface Props {
+    data: {
+        label?: string
+        description?: string
+        code?: CodeConfig
+        environment?: Environment
+        [key: string]: any
+    }
+    selected?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+    selected: false
+})
+
+// 语言图标映射
+const languageIcons: Record<string, string> = {
+    javascript: 'lucide:file-code',
+    python: 'lucide:file-code-2',
+    groovy: 'lucide:coffee',
+    java: 'lucide:coffee'
+}
+
+// 语言颜色映射
+const languageColors: Record<string, { bg: string, text: string, badge: string }> = {
+    javascript: { bg: '#fef3c7', text: '#f59e0b', badge: '#fbbf24' },
+    python: { bg: '#dbeafe', text: '#3b82f6', badge: '#60a5fa' },
+    groovy: { bg: '#e0e7ff', text: '#6366f1', badge: '#818cf8' },
+    java: { bg: '#fecaca', text: '#dc2626', badge: '#f87171' }
+}
+
+const language = computed(() => props.data.code?.language || 'javascript')
+const languageIcon = computed(() => languageIcons[language.value] || 'lucide:file-code')
+const languageColor = computed(() => languageColors[language.value] || languageColors.javascript)
+
+// 获取代码预览(前3行)
+const codePreview = computed(() => {
+    const code = props.data.code?.content || '// 编写代码'
+    const lines = code.split('\n').slice(0, 3)
+    return lines.join('\n')
+})
+
+console.log(props.data)
+// 代码行数
+const codeLines = computed(() => {
+    const code = props.data.code?.content || ''
+    return code.split('\n').length
+})
+</script>
+
+<template>
+    <div class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="bg-gradient-to-br from-white to-purple-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-purple-500 shadow-purple-200 shadow-lg' : 'border-purple-300 hover:shadow-lg hover:shadow-purple-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-purple-500 to-purple-400 rounded-l-xl">
+            </div>
+
+            <!-- 头部 -->
+            <div class="flex items-center gap-3 px-4 py-3 border-b border-purple-100">
+                <!-- 图标 -->
+                <div
+                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-400 rounded-lg shadow-md shadow-purple-200">
+                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+                    <Icon icon="lucide:code-2" color="#ffffff" class="relative z-10" :size="20" />
+                </div>
+
+                <!-- 标题 -->
+                <div class="flex-1 min-w-0">
+                    <div class="text-sm font-semibold text-gray-800 truncate">
+                        {{ data.label || '代码执行' }}
+                    </div>
+                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                        {{ data.description }}
+                    </div>
+                </div>
+
+                <!-- 语言标签 -->
+                <div class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold uppercase" :style="{
+                    backgroundColor: languageColor?.badge || '#fef3c7',
+                    color: 'white'
+                }">
+                    {{ language }}
+                </div>
+            </div>
+
+            <!-- 代码信息 -->
+            <div class="px-4 py-3 space-y-3">
+                <!-- 代码预览 -->
+                <div class="flex items-start gap-2">
+                    <Icon :icon="languageIcon" :color="languageColor?.text || '#6b7280'" :size="14"
+                        class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="flex items-center justify-between mb-1">
+                            <span class="text-xs text-gray-500">代码片段</span>
+                            <span class="text-xs text-gray-400">{{ codeLines }} 行</span>
+                        </div>
+                        <div class="relative">
+                            <pre
+                                class="text-xs font-mono bg-gray-900 text-gray-100 px-3 py-2 rounded border border-gray-700 overflow-hidden max-h-16">{{ codePreview }}</pre>
+                            <div
+                                class="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-gray-900 to-transparent pointer-events-none">
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 输入变量 -->
+                <div v-if="data.code?.inputVars && data.code.inputVars.length > 0" class="flex items-start gap-2">
+                    <Icon icon="lucide:import" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-1">输入变量</div>
+                        <div class="flex flex-wrap gap-1">
+                            <span v-for="varName in data.code.inputVars" :key="varName"
+                                class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-200 font-mono">
+                                <Icon icon="lucide:arrow-down-to-line" :size="10" />
+                                {{ varName }}
+                            </span>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 输出变量 -->
+                <div v-if="data.code?.outputVar" class="flex items-start gap-2">
+                    <Icon icon="lucide:export" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-1">输出变量</div>
+                        <div
+                            class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs rounded border border-green-200 font-mono">
+                            <Icon icon="lucide:arrow-up-from-line" :size="10" />
+                            {{ data.code.outputVar }}
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 执行环境 -->
+                <div v-if="data.environment?.timeout || data.environment?.memory"
+                    class="flex items-center gap-3 pt-2 border-t border-purple-100">
+                    <div v-if="data.environment.timeout" class="flex items-center gap-1.5">
+                        <Icon icon="lucide:timer" color="#94a3b8" :size="12" />
+                        <span class="text-xs text-gray-600">
+                            <span class="text-gray-500">超时:</span>
+                            <span class="font-medium ml-1">{{ data.environment.timeout }}ms</span>
+                        </span>
+                    </div>
+                    <div v-if="data.environment.memory" class="flex items-center gap-1.5">
+                        <Icon icon="lucide:memory-stick" color="#94a3b8" :size="12" />
+                        <span class="text-xs text-gray-600">
+                            <span class="text-gray-500">内存:</span>
+                            <span class="font-medium ml-1">{{ data.environment.memory }}MB</span>
+                        </span>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 底部状态栏 -->
+            <div class="flex items-center justify-between px-4 py-2 bg-purple-50/50 border-t border-purple-100">
+                <div class="flex items-center gap-2">
+                    <div class="flex items-center gap-1.5">
+                        <div class="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
+                        <span class="text-xs text-gray-500">就绪</span>
+                    </div>
+                    <!-- 运行状态 -->
+                    <div class="flex items-center gap-1 px-1.5 py-0.5 bg-purple-100 rounded">
+                        <Icon icon="lucide:zap" color="#9333ea" :size="10" />
+                        <span class="text-xs text-purple-700 font-medium">待执行</span>
+                    </div>
+                </div>
+                <div class="flex items-center gap-1">
+                    <Icon icon="lucide:play-circle" color="#94a3b8" :size="14"
+                        class="cursor-pointer hover:text-purple-500 transition-colors" title="运行代码" />
+                    <Icon icon="lucide:file-edit" color="#94a3b8" :size="14"
+                        class="cursor-pointer hover:text-purple-500 transition-colors" title="编辑代码" />
+                    <Icon icon="lucide:settings" color="#94a3b8" :size="14"
+                        class="cursor-pointer hover:text-purple-500 transition-colors" title="配置" />
+                </div>
+            </div>
+        </div>
+
+        <!-- 输入连接点 -->
+        <CanvasHandle handle-id="code-node-id" handle-type="target" :position="Position.Left" />
+        <!-- 输出连接点 -->
+        <CanvasHandle handle-id="code-node-id" handle-type="source" :position="Position.Right" />
+    </div>
+</template>
+
+<style scoped>
+pre {
+    line-height: 1.4;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+}
+
+
+.overflow-y-auto::-webkit-scrollbar {
+    width: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-track {
+    background: #f3e8ff;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb {
+    background: #9333ea;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb:hover {
+    background: #7e22ce;
+}
+</style>

+ 173 - 0
packages/workflow/src/components/elements/node-temp/ConditionNode.vue

@@ -0,0 +1,173 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 16:56:14
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 19:03:49
+ * @Describe: 条件节点
+-->
+<script setup lang="ts">
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import { Icon } from '@repo/ui'
+
+interface Condition {
+    id: string
+    name: string
+    expression: string
+    priority?: number
+}
+
+interface Props {
+    data: {
+        label?: string
+        description?: string
+        conditions?: Condition[]
+        defaultBranch?: string
+        [key: string]: any
+    }
+    selected?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+    selected: false
+})
+
+// 如果没有条件,至少显示默认分支
+const branches = props.data.conditions && props.data.conditions.length > 0
+    ? props.data.conditions
+    : [{ id: 'default', name: '默认分支', expression: 'true' }]
+
+// 计算每个分支 Handle 的位置
+const getBranchPosition = (index: number, total: number) => {
+    if (total === 1) return 50
+    const spacing = 70 / (total - 1) // 从 20% 到 90% 分布
+    return 15 + (index * spacing)
+}
+</script>
+
+<template>
+    <div class="relative min-w-[260px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="bg-gradient-to-br from-white to-orange-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-orange-500 shadow-orange-200 shadow-lg' : 'border-orange-300 hover:shadow-lg hover:shadow-orange-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-orange-500 to-orange-400 rounded-l-xl">
+            </div>
+
+            <!-- 头部 -->
+            <div class="flex items-center gap-3 px-4 py-3 border-b border-orange-100">
+                <!-- 图标 - 菱形形状 -->
+                <div
+                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-orange-500 to-orange-400 rounded-lg shadow-md shadow-orange-200 rotate-45">
+                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+                    <div class="-rotate-45">
+                        <Icon icon="lucide:git-branch" color="#ffffff" class="relative z-10" :size="20" />
+                    </div>
+                </div>
+
+                <!-- 标题 -->
+                <div class="flex-1 min-w-0">
+                    <div class="text-sm font-semibold text-gray-800 truncate">
+                        {{ data.label || '条件分支' }}
+                    </div>
+                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                        {{ data.description }}
+                    </div>
+                </div>
+
+                <!-- 分支数量标签 -->
+                <div
+                    class="flex-shrink-0 flex items-center gap-1 px-2 py-1 bg-orange-100 rounded text-xs font-medium text-orange-700">
+                    <Icon icon="lucide:split" color="#ea580c" :size="12" />
+                    <span>{{ branches.length }}</span>
+                </div>
+            </div>
+
+            <!-- 分支列表 -->
+            <div class="px-4 py-3 space-y-2 max-h-[200px] overflow-y-auto">
+                <div v-for="(condition, index) in branches" :key="condition.id"
+                    class="flex items-start gap-2 p-2 rounded-lg bg-orange-50/50 hover:bg-orange-100/50 transition-colors group">
+                    <!-- 分支序号 -->
+                    <div
+                        class="flex-shrink-0 flex items-center justify-center w-5 h-5 bg-orange-500 text-white text-xs font-bold rounded-full">
+                        {{ index + 1 }}
+                    </div>
+
+                    <!-- 分支信息 -->
+                    <div class="flex-1 min-w-0">
+                        <div class="flex items-center gap-1.5 mb-1">
+                            <span class="text-xs font-medium text-gray-700 truncate">
+                                {{ condition.name }}
+                            </span>
+                            <Icon v-if="condition.priority" icon="lucide:star" color="#f97316" :size="12"
+                                class="flex-shrink-0" />
+                        </div>
+                        <div class="text-xs text-gray-500 font-mono bg-white px-2 py-1 rounded truncate">
+                            {{ condition.expression }}
+                        </div>
+                    </div>
+
+                    <!-- 分支指示箭头 -->
+                    <div class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
+                        <Icon icon="lucide:arrow-right" color="#f97316" :size="14" />
+                    </div>
+                </div>
+
+                <!-- 默认分支提示 -->
+                <div v-if="data.defaultBranch"
+                    class="flex items-center gap-2 p-2 rounded-lg bg-gray-50 border border-dashed border-gray-300">
+                    <Icon icon="lucide:shield-check" color="#94a3b8" :size="14" />
+                    <span class="text-xs text-gray-500">
+                        默认: <span class="font-medium text-gray-700">{{ data.defaultBranch }}</span>
+                    </span>
+                </div>
+            </div>
+
+            <!-- 底部状态栏 -->
+            <div class="flex items-center justify-between px-4 py-2 bg-orange-50/50 border-t border-orange-100">
+                <div class="flex items-center gap-1.5">
+                    <Icon icon="lucide:zap" color="#f97316" :size="12" />
+                    <span class="text-xs text-gray-500">条件判断</span>
+                </div>
+                <Icon icon="lucide:settings" color="#94a3b8" :size="14"
+                    class="cursor-pointer hover:text-orange-500 transition-colors" />
+            </div>
+        </div>
+
+        <!-- 输入连接点 -->
+        <CanvasHandle handle-id="condition-node-id" handle-type="target" :position="Position.Left" />
+
+        <!-- 输出连接点 - 为每个分支创建一个 -->
+        <CanvasHandle v-for="(condition, index) in branches" :key="condition.id" :id="`branch-${condition.id}`"
+            handle-id="condition-node-id" handle-type="source" :position="Position.Right" :style="{
+                top: `${getBranchPosition(index, branches.length)}%`,
+            }">
+            <!-- 多个分支标签提示 -->
+            <div
+                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-orange-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
+                {{ condition.name }}
+            </div>
+        </CanvasHandle>
+    </div>
+</template>
+
+<style scoped>
+.overflow-y-auto::-webkit-scrollbar {
+    width: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-track {
+    background: #fed7aa;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb {
+    background: #f97316;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb:hover {
+    background: #ea580c;
+}
+</style>

+ 243 - 0
packages/workflow/src/components/elements/node-temp/DataBaseNode.vue

@@ -0,0 +1,243 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 16:57:09
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 19:09:53
+ * @Describe: 数据查询节点
+-->
+<script setup lang="ts">
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import { Icon } from '@repo/ui'
+import { computed } from 'vue'
+
+interface Datasource {
+    type: 'mysql' | 'postgresql' | 'mongodb' | 'redis' | 'api'
+    connectionId: string
+    connectionName?: string
+}
+
+interface Query {
+    type: 'sql' | 'nosql' | 'rest'
+    content: string
+    params?: Record<string, any>
+}
+
+interface Props {
+    data: {
+        label?: string
+        description?: string
+        datasource?: Datasource
+        query?: Query
+        result?: {
+            limit?: number
+            mapping?: Record<string, string>
+        }
+        [key: string]: any
+    }
+    selected?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+    selected: false
+})
+
+// 数据源类型图标映射
+const datasourceIcons: Record<string, string> = {
+    mysql: 'lucide:database',
+    postgresql: 'lucide:database',
+    mongodb: 'lucide:database',
+    redis: 'lucide:hard-drive',
+    api: 'lucide:cloud'
+}
+
+// 数据源类型颜色映射
+const datasourceColors: Record<string, string> = {
+    mysql: '#13c2c2',
+    postgresql: '#13c2c2',
+    mongodb: '#52c41a',
+    redis: '#ff4d4f',
+    api: '#1890ff'
+}
+
+const datasourceType = computed(() => props.data.datasource?.type || 'mysql')
+const datasourceIcon = computed(() => datasourceIcons[datasourceType.value] || 'lucide:database')
+const datasourceColor = computed(() => datasourceColors[datasourceType.value] || '#13c2c2')
+
+// 查询类型标签
+const queryTypeLabel = computed(() => {
+    const type = props.data.query?.type
+    return type?.toUpperCase() || 'SQL'
+})
+</script>
+
+<template>
+    <div class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="bg-gradient-to-br from-white to-cyan-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-cyan-500 shadow-cyan-200 shadow-lg' : 'border-cyan-300 hover:shadow-lg hover:shadow-cyan-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-cyan-500 to-cyan-400 rounded-l-xl">
+            </div>
+
+            <!-- 头部 -->
+            <div class="flex items-center gap-3 px-4 py-3 border-b border-cyan-100">
+                <!-- 图标 -->
+                <div
+                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-cyan-500 to-cyan-400 rounded-lg shadow-md shadow-cyan-200">
+                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+                    <Icon :icon="datasourceIcon" color="#ffffff" class="relative z-10" :size="20" />
+                </div>
+
+                <!-- 标题 -->
+                <div class="flex-1 min-w-0">
+                    <div class="text-sm font-semibold text-gray-800 truncate">
+                        {{ data.label || '数据查询' }}
+                    </div>
+                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                        {{ data.description }}
+                    </div>
+                </div>
+
+                <!-- 查询类型标签 -->
+                <div class="flex-shrink-0 px-2 py-1 bg-cyan-100 rounded text-xs font-bold text-cyan-700">
+                    {{ queryTypeLabel }}
+                </div>
+            </div>
+
+            <!-- 数据源信息 -->
+            <div class="px-4 py-3 space-y-3">
+                <!-- 数据源 -->
+                <div class="flex items-start gap-2">
+                    <Icon icon="lucide:server" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-1">数据源</div>
+                        <div class="flex items-center gap-2">
+                            <div class="w-2 h-2 rounded-full" :style="{ backgroundColor: datasourceColor }"></div>
+                            <span class="text-xs font-medium text-gray-700 uppercase">
+                                {{ datasourceType }}
+                            </span>
+                            <span v-if="data.datasource?.connectionName" class="text-xs text-gray-500">
+                                - {{ data.datasource.connectionName }}
+                            </span>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 查询语句 -->
+                <div class="flex items-start gap-2">
+                    <Icon icon="lucide:code-2" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-1">查询语句</div>
+                        <div
+                            class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-2 rounded border border-gray-200 max-h-20 overflow-y-auto">
+                            {{ data.query?.content || 'SELECT * FROM table_name' }}
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 查询参数 -->
+                <div v-if="data.query?.params && Object.keys(data.query.params).length > 0"
+                    class="flex items-start gap-2">
+                    <Icon icon="lucide:list-filter" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-1">查询参数</div>
+                        <div class="flex flex-wrap gap-1">
+                            <span v-for="(value, key) in data.query.params" :key="key"
+                                class="inline-flex items-center gap-1 px-2 py-0.5 bg-cyan-50 text-cyan-700 text-xs rounded border border-cyan-200">
+                                <span class="font-medium">{{ key }}:</span>
+                                <span class="font-mono">{{ value }}</span>
+                            </span>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 结果配置 -->
+                <div v-if="data.result?.limit" class="flex items-center gap-2">
+                    <Icon icon="lucide:layers" color="#94a3b8" :size="14" class="flex-shrink-0" />
+                    <div class="text-xs text-gray-600">
+                        <span class="text-gray-500">返回条数:</span>
+                        <span class="font-medium ml-1">{{ data.result.limit }}</span>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 底部状态栏 -->
+            <div class="flex items-center justify-between px-4 py-2 bg-cyan-50/50 border-t border-cyan-100">
+                <div class="flex items-center gap-2">
+                    <div class="flex items-center gap-1.5">
+                        <div class="w-1.5 h-1.5 bg-cyan-500 rounded-full animate-pulse"></div>
+                        <span class="text-xs text-gray-500">就绪</span>
+                    </div>
+                    <!-- 连接状态 -->
+                    <div class="flex items-center gap-1 px-1.5 py-0.5 bg-green-100 rounded">
+                        <Icon icon="lucide:check-circle-2" color="#52c41a" :size="10" />
+                        <span class="text-xs text-green-700 font-medium">已连接</span>
+                    </div>
+                </div>
+                <div class="flex items-center gap-1">
+                    <Icon icon="lucide:play" color="#94a3b8" :size="14"
+                        class="cursor-pointer hover:text-cyan-500 transition-colors" title="测试查询" />
+                    <Icon icon="lucide:settings" color="#94a3b8" :size="14"
+                        class="cursor-pointer hover:text-cyan-500 transition-colors" title="配置" />
+                </div>
+            </div>
+        </div>
+
+        <!-- 输入连接点 -->
+        <CanvasHandle handle-id="data-node-id" handle-type="target" :position="Position.Left" />
+
+        <!-- 输出连接点 - 成功 -->
+        <CanvasHandle handle-id="success" handle-type="source" :position="Position.Right" :style="{ top: '40%' }">
+            <div
+                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-green-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
+                成功
+            </div>
+        </CanvasHandle>
+
+        <!-- 输出连接点 - 失败 -->
+        <CanvasHandle handle-id="error" handle-type="source" :position="Position.Right" :style="{ top: '60%' }">
+            <div
+                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-red-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
+                失败
+            </div>
+        </CanvasHandle>
+    </div>
+</template>
+
+<style scoped>
+.overflow-y-auto::-webkit-scrollbar {
+    width: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-track {
+    background: #cffafe;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb {
+    background: #06b6d4;
+    border-radius: 4px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb:hover {
+    background: #0891b2;
+}
+
+@keyframes pulse {
+
+    0%,
+    100% {
+        opacity: 1;
+    }
+
+    50% {
+        opacity: 0.5;
+    }
+}
+
+.animate-pulse {
+    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+</style>

+ 75 - 0
packages/workflow/src/components/elements/node-temp/EndNode.vue

@@ -0,0 +1,75 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 15:24:20
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 18:49:09
+ * @Describe: 结束节点
+-->
+<script lang="ts" setup>
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import type { NodeProps } from '@vue-flow/core'
+import { Icon } from '@repo/ui'
+
+const props = withDefaults(defineProps<NodeProps>(), {
+    selected: true
+})
+
+</script>
+<template>
+    <div class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-green-500 shadow-green-200 shadow-lg' : 'border-green-300 hover:shadow-lg hover:shadow-green-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl">
+            </div>
+
+            <!-- 图标区域 -->
+            <div
+                class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200">
+                <!-- 光泽效果 -->
+                <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+
+                <!-- 图标 -->
+                <Icon icon="lucide:unplug" color="#ffffff" class="relative z-10" :size="20" />
+            </div>
+
+            <!-- 内容区域 -->
+            <div class="flex-1 min-w-0">
+                <div class="text-sm font-semibold text-gray-800 truncate">
+                    {{ data.label || '结束' }}
+                </div>
+                <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                    {{ data.description }}
+                </div>
+            </div>
+        </div>
+
+        <!-- 输出连接点 -->
+        <CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
+    </div>
+</template>
+<style lang="less" scoped>
+@keyframes pulse {
+
+    0%,
+    100% {
+        opacity: 1;
+    }
+
+    50% {
+        opacity: 0.5;
+    }
+}
+
+@keyframes ping {
+
+    75%,
+    100% {
+        transform: scale(2);
+        opacity: 0;
+    }
+}
+</style>

+ 125 - 0
packages/workflow/src/components/elements/node-temp/HttpNode1.vue

@@ -0,0 +1,125 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 15:18:09
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 19:05:43
+ * @Describe: http节点
+-->
+<script setup lang="ts">
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import { Icon } from '@repo/ui'
+
+interface HttpConfig {
+    method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
+    url?: string
+    timeout?: number
+}
+
+interface Props {
+    data: {
+        label?: string
+        description?: string
+        config?: HttpConfig
+        [key: string]: any
+    }
+    selected?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+    selected: false
+})
+
+console.log(props.data.id, '1212121')
+// 请求方法对应的颜色
+const methodColors: Record<string, string> = {
+    GET: '#1890ff',
+    POST: '#52c41a',
+    PUT: '#faad14',
+    DELETE: '#ff4d4f',
+    PATCH: '#722ed1'
+}
+
+const currentMethod = props.data.config?.method || 'GET'
+const methodColor = methodColors[currentMethod]
+</script>
+
+<template>
+    <div class="relative min-w-[240px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="bg-gradient-to-br from-white to-blue-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-blue-500 shadow-blue-200 shadow-lg' : 'border-blue-300 hover:shadow-lg hover:shadow-blue-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-blue-500 to-blue-400 rounded-l-xl">
+            </div>
+
+            <!-- 头部 -->
+            <div class="flex items-center gap-3 px-4 py-3 border-b border-blue-100">
+                <!-- 图标 -->
+                <div
+                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-400 rounded-lg shadow-md shadow-blue-200">
+                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+                    <Icon icon="lucide:cloud" color="#ffffff" class="relative z-10" :size="20" />
+                </div>
+
+                <!-- 标题 -->
+                <div class="flex-1 min-w-0">
+                    <div class="text-sm font-semibold text-gray-800 truncate">
+                        {{ data.label || 'HTTP 请求' }}
+                    </div>
+                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                        {{ data.description }}
+                    </div>
+                </div>
+
+                <!-- 请求方法标签 -->
+                <div class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold text-white"
+                    :style="{ backgroundColor: methodColor }">
+                    {{ currentMethod }}
+                </div>
+            </div>
+
+            <!-- 配置信息 -->
+            <div class="px-4 py-3 space-y-2">
+                <!-- URL -->
+                <div class="flex items-start gap-2">
+                    <Icon icon="lucide:link" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+                    <div class="flex-1 min-w-0">
+                        <div class="text-xs text-gray-500 mb-0.5">请求地址</div>
+                        <div class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-1 rounded truncate">
+                            {{ data.config?.url || '未配置' }}
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 超时时间 -->
+                <div v-if="data.config?.timeout" class="flex items-center gap-2">
+                    <Icon icon="lucide:clock" color="#94a3b8" :size="14" class="flex-shrink-0" />
+                    <div class="text-xs text-gray-600">
+                        <span class="text-gray-500">超时:</span>
+                        <span class="font-medium ml-1">{{ data.config.timeout }}ms</span>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 底部状态栏 -->
+            <div class="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-t border-blue-100">
+                <div class="flex items-center gap-1.5">
+                    <div class="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
+                    <span class="text-xs text-gray-500">就绪</span>
+                </div>
+                <Icon icon="lucide:settings" color="#94a3b8" :size="14"
+                    class="cursor-pointer hover:text-blue-500 transition-colors" />
+            </div>
+        </div>
+
+        <!-- 输入连接点 -->
+        <CanvasHandle :handle-id="props.data.id" handle-type="target" :position="Position.Left" />
+
+        <!-- 输出连接点 -->
+        <CanvasHandle :handle-id="props.data.id" handle-type="source" :position="Position.Right" />
+    </div>
+</template>
+
+<style scoped lang="less"></style>

+ 76 - 0
packages/workflow/src/components/elements/node-temp/StartNode.vue

@@ -0,0 +1,76 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-01-25 15:24:20
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 19:06:20
+ * @Describe: 开始节点
+-->
+<script lang="ts" setup>
+import { Position } from '@vue-flow/core'
+import CanvasHandle from '../handles/CanvasHandle.vue'
+import type { NodeProps } from '@vue-flow/core'
+import { Icon } from '@repo/ui'
+
+const props = withDefaults(defineProps<NodeProps>(), {
+    selected: false
+})
+
+</script>
+<template>
+    <div class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+        :class="{ 'scale-105': selected }">
+        <!-- 节点主体 -->
+        <div class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+            :class="selected ? 'border-green-500 shadow-green-200 shadow-lg' : 'border-green-300 hover:shadow-lg hover:shadow-green-100'">
+            <!-- 左侧装饰条 -->
+            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl">
+            </div>
+
+            <!-- 图标区域 -->
+            <div
+                class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200">
+                <!-- 光泽效果 -->
+                <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+
+                <!-- 播放图标 -->
+                <Icon icon="lucide:play" color="#ffffff" class="relative z-10" :size="20" />
+            </div>
+
+            <!-- 内容区域 -->
+            <div class="flex-1 min-w-0">
+                <div class="text-sm font-semibold text-gray-800 truncate">
+                    {{ data.label || '开始' }}
+                </div>
+                <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+                    {{ data.description }}
+                </div>
+            </div>
+
+        </div>
+
+        <!-- 输出连接点 -->
+        <CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
+    </div>
+</template>
+<style lang="less" scoped>
+@keyframes pulse {
+
+    0%,
+    100% {
+        opacity: 1;
+    }
+
+    50% {
+        opacity: 0.5;
+    }
+}
+
+@keyframes ping {
+
+    75%,
+    100% {
+        transform: scale(2);
+        opacity: 0;
+    }
+}
+</style>

+ 35 - 4
pnpm-lock.yaml

@@ -136,6 +136,9 @@ importers:
 
   apps/web:
     dependencies:
+      '@repo/nodes':
+        specifier: workspace:^
+        version: link:../../packages/nodes
       element-plus:
         specifier: ^2.13.1
         version: 2.13.1(vue@3.5.27(typescript@5.9.3))
@@ -152,9 +155,6 @@ importers:
       '@repo/workflow':
         specifier: workspace:*
         version: link:../../packages/workflow
-      '@types/node':
-        specifier: ^24.10.1
-        version: 24.10.9
       '@vitejs/plugin-vue':
         specifier: ^6.0.1
         version: 6.0.3(rolldown-vite@7.2.5(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(yaml@1.10.2))(vue@3.5.27(typescript@5.9.3))
@@ -238,7 +238,30 @@ importers:
         specifier: ^8.50.0
         version: 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.2)
 
-  packages/nodes: {}
+  packages/nodes:
+    dependencies:
+      less:
+        specifier: ^4.5.1
+        version: 4.5.1
+      less-loader:
+        specifier: ^12.3.0
+        version: 12.3.0(@rspack/core@1.7.3(@swc/helpers@0.5.18))(less@4.5.1)
+    devDependencies:
+      '@repo/nodes':
+        specifier: workspace:*
+        version: 'link:'
+      '@repo/typescript-config':
+        specifier: workspace:*
+        version: link:../typescript-config
+      '@repo/workflow':
+        specifier: workspace:*
+        version: link:../workflow
+      tailwindcss:
+        specifier: ^4.1.18
+        version: 4.1.18
+      vue:
+        specifier: ^3.5.24
+        version: 3.5.27(typescript@5.9.3)
 
   packages/typescript-config: {}
 
@@ -283,6 +306,9 @@ importers:
         specifier: ^3.5.24
         version: 3.5.27(typescript@5.9.3)
     devDependencies:
+      '@repo/nodes':
+        specifier: workspace:*
+        version: link:../nodes
       '@repo/typescript-config':
         specifier: workspace:*
         version: link:../typescript-config
@@ -5200,6 +5226,9 @@ packages:
     resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==}
     hasBin: true
 
+  tailwindcss@4.1.18:
+    resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
+
   text-mapping@1.0.1:
     resolution: {integrity: sha512-lONDMFNJ6QPjxYPcxkAy420qB+jMRxXERkNJAVDapQeZs0POW4pWryG9R+kAer6H+VDh/Z07x9GMNhhQrnSVIw==}
 
@@ -12176,6 +12205,8 @@ snapshots:
     transitivePeerDependencies:
       - encoding
 
+  tailwindcss@4.1.18: {}
+
   text-mapping@1.0.1: {}
 
   text-table@0.2.0: {}