Explorar el Código

feat:新增添加节点功能及节点规范

lj1559651600@163.com hace 1 semana
padre
commit
ea43da9af0

+ 9 - 0
packages/nodes/index.ts

@@ -0,0 +1,9 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-24 19:21:42
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-25 21:12:44
+ * @Describe: file describe
+ */
+
+export *  from './materials';

+ 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",

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

@@ -1,3 +1,4 @@
+
 import type { Node, DefaultEdge } from '@vue-flow/core'
 
 export type CanvasConnectionPort = {
@@ -7,6 +8,7 @@ export type CanvasConnectionPort = {
 	required?: boolean
 	maxConnections?: number
 	label?: string
+    [key: string]: any
 }
 
 export interface IWorkflowNode extends Node {

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

+ 13 - 15
packages/workflow/src/components/elements/CanvasNode.vue

@@ -9,21 +9,19 @@ const props = defineProps<NodeProps>()
 </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" />
-		</div>
+    <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" />
+        </div>
 
-		<div className="absolute w-full bottom--22px text-12px text-center text-#222">
-			{{ data.label }}
-		</div>
-		<div className="absolute w-full bottom--38px text-12px text-center text-#999 truncate">
-			{{ data.subtitle }}
-		</div>
+        <div className="absolute w-full bottom--22px text-12px text-center text-[#222]">
+            {{ data.label }}
+        </div>
+        <div className="absolute w-full bottom--38px text-12px text-center text-[#999] truncate">
+            {{ data.subtitle }}
+        </div>
 
-		<CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
-		<CanvasHandle handle-id="2" handle-type="target" :position="Position.Left" />
-	</div>
+        <CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
+        <CanvasHandle handle-id="2" handle-type="target" :position="Position.Left" />
+    </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>

+ 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: {}