소스 검색

feat: 添加http节点配置

jiaxing.liao 3 주 전
부모
커밋
94c3597274

+ 2 - 0
apps/web/package.json

@@ -13,12 +13,14 @@
     "@repo/nodes": "workspace:^",
     "echarts": "^6.0.0",
     "element-plus": "^2.13.1",
+    "lodash-es": "^4.17.21",
     "monaco-editor": "^0.55.1",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.4",
     "uuid": "^13.0.0",
     "vue": "^3.5.24",
     "vue-element-plus-x": "^1.3.98",
+    "vue-hooks-plus": "^2.4.1",
     "vue-router": "4"
   },
   "devDependencies": {

+ 461 - 15
apps/web/src/components/setter/HttpSetter.vue

@@ -6,27 +6,473 @@
  * @Describe: http设置器
 -->
 <script lang="ts" setup>
-import { ElDrawer, ElButton } from 'element-plus';
-import { Icon } from '@iconify/vue';
+import { watch, ref } from 'vue'
+import { Input, IconButton } from '@repo/ui'
+import { clone, isEqual } from 'lodash-es'
+
 const props = withDefaults(
-    defineProps<{
-        data: any,
-        visible: boolean,
-    }>(),
-    {
-        visible: false,
-        data: {}
-    }
-);
+	defineProps<{
+		data: any
+	}>(),
+	{
+		data: {}
+	}
+)
+
 const emit = defineEmits<{
-    'update:visible': [value: boolean]
+	update: [data: unknown]
 }>()
 
+const DEFAULT_DATA = {
+	method: 'GET',
+	url: '',
+	headers: [],
+	params: [],
+	bodyType: 'json',
+	body: '',
+	verifySSL: true,
+	timeoutConfig: {
+		connect: 8,
+		read: 6,
+		write: 1
+	},
+	output: {
+		body: '',
+		status_code: 200,
+		headers: [],
+		files: []
+	},
+	errorConfig: {
+		retry: true,
+		max_retry: 3,
+		retry_delay: 100
+	},
+	exception: 'none',
+	exceptionDefaultValue: {
+		body: '',
+		status_code: 0,
+		headers: '{}'
+	}
+}
+
+const formData = ref(clone(DEFAULT_DATA))
+
+watch(
+	() => props.data,
+	(newVal) => {
+		if (!isEqual(newVal, formData.value)) {
+			formData.value = {
+				...clone(DEFAULT_DATA),
+				...(newVal || {})
+			}
+		}
+	},
+	{
+		deep: true,
+		immediate: true
+	}
+)
+
+const methodOptions = [
+	{ label: 'GET', value: 'GET' },
+	{ label: 'POST', value: 'POST' },
+	{ label: 'PUT', value: 'PUT' },
+	{ label: 'DELETE', value: 'DELETE' },
+	{ label: 'PATCH', value: 'PATCH' }
+]
+
+const bodyTypeOptions = [
+	{ label: 'none', value: 'none' },
+	{ label: 'form-data', value: 'form-data' },
+	{ label: 'x-www-form-urlencoded', value: 'x-www-form-urlencoded' },
+	{ label: 'raw', value: 'raw' },
+	{ label: 'json', value: 'json' },
+	{ label: 'binary', value: 'binary' }
+]
+
+const exceptionOptions = [
+	{ label: '无', value: 'none' },
+	{ label: '默认值', value: 'default_value' },
+	{ label: '异常分支', value: 'exception_branch' }
+]
+
+const headers = ref([{ key: '', value: '' }])
+const params = ref([{ key: '', value: '' }])
+const body = ref<string | Record<string, any>[]>('')
+
+watch(
+	() => formData.value,
+	(value) => {
+		emit('update', value)
+	},
+	{ deep: true }
+)
+
+watch(
+	() => headers.value,
+	(val) => {
+		formData.value.headers = val.filter((item) => item.key && item.value)
+	}
+)
+
+watch(
+	() => params.value,
+	(val) => {
+		formData.value.params = val.filter((item) => item.key && item.value)
+	}
+)
+
+watch(
+	() => body.value,
+	(val) => {
+		if (typeof val === 'object') {
+			val = val.filter((item) => item.key)
+		}
+		formData.value.body = val
+	}
+)
+
+const handleAddHeader = (index: number) => {
+	if (index === headers.value.length - 1) {
+		headers.value.push({ key: '', value: '' })
+	}
+}
+
+const handleAddParam = (index: number) => {
+	if (index === params.value.length - 1) {
+		params.value.push({ key: '', value: '' })
+	}
+}
+
+const handleDeleteHeader = (index: number) => {
+	headers.value.splice(index, 1)
+	if (headers.value.length === 0) {
+		headers.value.push({ key: '', value: '' })
+	}
+}
+const handleDeleteParam = (index: number) => {
+	params.value.splice(index, 1)
+	if (params.value.length === 0) {
+		params.value.push({ key: '', value: '' })
+	}
+}
+
+const formDataItem = { key: '', value: '', type: 'text' }
+const defaultItem = { key: '', value: '' }
+
+const handleChangeBodyType = (type: string) => {
+	if (['form-data', 'x-www-form-urlencoded'].includes(type)) {
+		const item = type === 'form-data' ? formDataItem : defaultItem
+
+		body.value = [{ ...item }]
+		formData.value.body = [{ ...item }]
+	} else {
+		body.value = ''
+		formData.value.body = ''
+	}
+}
+
+const handleAddBody = (index: number) => {
+	if (index !== body.value.length - 1) return
+	const item = formData.value.bodyType === 'form-data' ? formDataItem : defaultItem
+	if (typeof body.value === 'string') return
+
+	body.value = [...body.value, { ...item }]
+}
+const handleDeleteBody = (index: number) => {
+	if (typeof body.value === 'string') return
+
+	body.value.splice(index, 1)
+	if (body.value.length === 0) {
+		const item = formData.value.bodyType === 'form-data' ? formDataItem : defaultItem
+		body.value = [{ ...item }]
+	}
+}
 </script>
+
 <template>
-    <div class='content'>
+	<el-scrollbar class="w-full box-border p-12px">
+		<el-form label-width="50px">
+			<el-form-item label="API" label-position="top">
+				<div class="w-full flex gap-8px">
+					<el-select
+						style="width: 100px"
+						:options="methodOptions"
+						v-model="formData.method"
+						placeholder="请选择"
+					>
+					</el-select>
+					<el-input class="flex-1" v-model="formData.url" placeholder="请输入"></el-input>
+				</div>
+			</el-form-item>
+
+			<el-form-item label="HEADERS" label-position="top">
+				<el-table :data="headers" border>
+					<el-table-column align="center" prop="key" label="键">
+						<template #default="{ row }">
+							<Input v-model="row.key" variant="borderless" placeholder="请输入" />
+						</template>
+					</el-table-column>
+					<el-table-column align="center" prop="value" label="值">
+						<template #default="{ row, $index }">
+							<div class="relative">
+								<Input
+									v-model="row.value"
+									variant="borderless"
+									placeholder="请输入"
+									@focus="handleAddHeader($index)"
+								/>
+								<IconButton
+									class="absolute right-0 top-5px"
+									icon="ep:delete"
+									link
+									@click="handleDeleteHeader($index)"
+								/>
+							</div>
+						</template>
+					</el-table-column>
+				</el-table>
+			</el-form-item>
+
+			<el-form-item label="PARAMS" label-position="top">
+				<el-table :data="params" border>
+					<el-table-column align="center" prop="key" label="键">
+						<template #default="{ row }">
+							<Input v-model="row.key" variant="borderless" placeholder="请输入" />
+						</template>
+					</el-table-column>
+					<el-table-column align="center" prop="value" label="值">
+						<template #default="{ row, $index }">
+							<div class="relative">
+								<Input
+									v-model="row.value"
+									variant="borderless"
+									placeholder="请输入"
+									@focus="handleAddParam($index)"
+								/>
+								<IconButton
+									class="absolute right-0 top-5px"
+									icon="ep:delete"
+									link
+									@click="handleDeleteParam($index)"
+								/>
+							</div>
+						</template>
+					</el-table-column>
+				</el-table>
+			</el-form-item>
+
+			<el-form-item label="BODY" label-position="top">
+				<el-radio-group v-model="formData.bodyType" @change="handleChangeBodyType">
+					<el-radio v-for="type in bodyTypeOptions" :key="type.value" :value="type.value">{{
+						type.label
+					}}</el-radio>
+				</el-radio-group>
+			</el-form-item>
+
+			<div
+				v-if="formData.bodyType === 'form-data' || formData.bodyType === 'x-www-form-urlencoded'"
+				class="mb-12px"
+			>
+				<el-table :data="body" border>
+					<el-table-column align="center" prop="key" label="键">
+						<template #default="{ row }">
+							<Input v-model="row.key" variant="borderless" placeholder="请输入" />
+						</template>
+					</el-table-column>
+					<el-table-column
+						v-if="formData.bodyType === 'form-data'"
+						align="center"
+						prop="type"
+						label="类型"
+					>
+						<template #default="{ row }">
+							<el-select
+								v-model="row.type"
+								placeholder="请选择"
+								:options="[
+									{ label: 'text', value: 'text' },
+									{ label: 'file', value: 'file' }
+								]"
+							/>
+						</template>
+					</el-table-column>
+					<el-table-column align="center" prop="value" label="值">
+						<template #default="{ row, $index }">
+							<div class="relative">
+								<Input
+									v-model="row.value"
+									variant="borderless"
+									placeholder="请输入"
+									@focus="handleAddBody($index)"
+								/>
+								<IconButton
+									class="absolute right-0 top-5px"
+									icon="ep:delete"
+									link
+									@click="handleDeleteBody($index)"
+								/>
+							</div>
+						</template>
+					</el-table-column>
+				</el-table>
+			</div>
+
+			<div v-if="['json', 'raw', 'binary'].includes(formData.bodyType)" class="mb-12px">
+				<el-input
+					v-model="formData.body"
+					:type="formData.bodyType != 'binary' ? 'textarea' : ''"
+					:placeholder="formData.bodyType != 'binary' ? '请输入' : '请输入变量'"
+					:autosize="{ minRows: 5, maxRows: 10 }"
+				/>
+			</div>
+
+			<el-form-item label="验证SSL证书" label-width="120px" label-position="left">
+				<div class="w-full text-right">
+					<el-switch v-model="formData.verifySSL"></el-switch>
+				</div>
+			</el-form-item>
 
+			<el-collapse>
+				<el-collapse-item title="超时设置" name="1">
+					<el-form-item label="连接超时" label-width="120px" label-position="top">
+						<el-input-number
+							v-model="formData.timeoutConfig.connect"
+							:min="1"
+							:max="10"
+							controls-position="right"
+							style="width: 100%"
+							suffix="s"
+							placeholder="请输入连接超时"
+						></el-input-number>
+					</el-form-item>
+					<el-form-item label="读取超时" label-width="120px" label-position="top">
+						<el-input-number
+							v-model="formData.timeoutConfig.read"
+							:min="1"
+							:max="10"
+							controls-position="right"
+							style="width: 100%"
+							suffix="s"
+							placeholder="请输入连接超时"
+						></el-input-number>
+					</el-form-item>
+					<el-form-item label="写入超时" label-width="120px" label-position="top">
+						<el-input-number
+							v-model="formData.timeoutConfig.write"
+							:min="1"
+							:max="10"
+							controls-position="right"
+							style="width: 100%"
+							suffix="s"
+							placeholder="请输入连接超时"
+						></el-input-number>
+					</el-form-item>
+				</el-collapse-item>
+				<el-collapse-item title="输出变量" name="2">
+					<ul>
+						<li>
+							<div>
+								<span class="text-#333">body</span>
+								<span class="text-#999 ml-8px">string</span>
+							</div>
+							<div class="text-#666">响应内容</div>
+						</li>
+						<li>
+							<div>
+								<span class="text-#333">status_code</span>
+								<span class="text-#999 ml-8px">number</span>
+							</div>
+							<div class="text-#666">响应状态码</div>
+						</li>
+						<li>
+							<div>
+								<span class="text-#333">headers</span>
+								<span class="text-#999 ml-8px">object</span>
+							</div>
+							<div class="text-#666">响应头列表JSON</div>
+						</li>
+						<li>
+							<div>
+								<span class="text-#333">files</span>
+								<span class="text-#999 ml-8px">Array[File]</span>
+							</div>
+							<div class="text-#666">文件列表</div>
+						</li>
+					</ul>
+				</el-collapse-item>
+			</el-collapse>
 
-    </div>
+			<el-form-item label="失败时重试" label-width="120px" label-position="left">
+				<div class="w-full text-right">
+					<el-switch v-model="formData.errorConfig.retry"></el-switch>
+				</div>
+			</el-form-item>
+			<div v-if="formData.errorConfig.retry" class="flex items-center mb-12px">
+				<div class="w-150px text-12px text-gray-500">最大重试次数</div>
+				<div class="flex-1 flex items-center gap-8px">
+					<el-slider
+						v-model="formData.errorConfig.max_retry"
+						:max="10"
+						:min="1"
+						:step="1"
+						style="flex: 1"
+					></el-slider>
+					<el-input-number
+						v-model="formData.errorConfig.max_retry"
+						:min="1"
+						:max="10"
+						controls-position="right"
+						style="flex: 1"
+					></el-input-number>
+				</div>
+			</div>
+			<div v-if="formData.errorConfig.retry" class="flex items-center mb-12px">
+				<div class="w-150px text-12px text-gray-500">重试次数间隔时间(ms)</div>
+				<div class="flex-1 flex items-center gap-8px">
+					<el-slider
+						v-model="formData.errorConfig.retry_delay"
+						:max="5000"
+						:min="100"
+						:step="1"
+						style="flex: 1"
+					></el-slider>
+					<el-input-number
+						v-model="formData.errorConfig.retry_delay"
+						:max="5000"
+						:min="100"
+						controls-position="right"
+						style="flex: 1"
+					></el-input-number>
+				</div>
+			</div>
+			<el-form-item label="异常处理" label-width="90px" label-position="left">
+				<div class="w-full text-right">
+					<el-select
+						v-model="formData.exception"
+						:options="exceptionOptions"
+						style="width: 120px"
+					/>
+				</div>
+			</el-form-item>
+			<div v-if="formData.exception === 'default_value'">
+				<div class="text-12px text-gray-500">当发生异常时,指定默认数据输出</div>
+				<el-form-item label="body" label-position="top">
+					<el-input v-model="formData.exceptionDefaultValue.body" type="textarea" rows="3" />
+				</el-form-item>
+				<el-form-item label="status_code" label-position="top">
+					<el-input-number
+						controls-position="right"
+						v-model="formData.exceptionDefaultValue.status_code"
+					/>
+				</el-form-item>
+				<el-form-item label="headers" label-position="top">
+					<el-input v-model="formData.exceptionDefaultValue.headers" type="textarea" rows="5" />
+				</el-form-item>
+			</div>
+			<div v-if="formData.exception === 'exception_branch'">
+				<div class="text-12px text-gray-500">请在画布定义异常处理逻辑!</div>
+			</div>
+		</el-form>
+	</el-scrollbar>
 </template>
-<style lang="scss" scoped></style>

+ 8 - 3
apps/web/src/components/setter/index.vue

@@ -34,11 +34,12 @@ interface Props {
 	visible: boolean
 }
 const props = withDefaults(defineProps<Props>(), {
-    visible: false,
-		id: '',
+	visible: false,
+	id: ''
 })
 const emit = defineEmits<{
 	'update:visible': [value: boolean]
+	'update:node:data': [id: string, data: Record<string, unknown>]
 }>()
 
 const node = computed(() => {
@@ -52,6 +53,10 @@ const setter = computed(() => {
 const closeDrawer = () => {
 	emit('update:visible', false)
 }
+
+const onUpdate = (data: Record<string, unknown>) => {
+	emit('update:node:data', props.id, data)
+}
 </script>
 <template>
 	<div class="setter">
@@ -67,7 +72,7 @@ const closeDrawer = () => {
 				></Icon>
 			</header>
 			<div class="content">
-				<component :is="setter" :data="node?.data"></component>
+				<component :is="setter" :data="node?.data" @update="onUpdate"></component>
 			</div>
 		</div>
 	</div>

+ 8 - 4
apps/web/src/views/Editor.vue

@@ -99,16 +99,16 @@ const handleRunWorkflow = () => {
 	console.log('run workflow')
 }
 const handleNodeCreate = (value: SourceType | string) => {
-	console.log(value)
-
+	const id = uuid()
 	if (typeof value === 'string') {
 		if (value === 'stickyNote') {
 			workflow.value.nodes.push({
-				id: uuid(),
+				id,
 				type: 'canvas-node',
 				zIndex: -1,
 				position: { x: 600, y: 300 },
 				data: {
+					id,
 					version: ['1.0.0'],
 					inputs: [],
 					outputs: [],
@@ -137,6 +137,10 @@ const handleNodeCreate = (value: SourceType | string) => {
 	if (nodeToAdd) {
 		workflow.value.nodes.push({
 			...nodeToAdd,
+			data: {
+				...nodeToAdd.data,
+				id
+			},
 			zIndex: 1,
 			id: uuid()
 		})
@@ -156,7 +160,6 @@ const handleDrop = (position: XYPosition, event: DragEvent) => {
  * 创建连线
  */
 const onCreateConnection = (connection: Connection) => {
-	console.log('create connection', connection)
 	const { source, target } = connection
 
 	if (!workflow.value.edges.some((edge) => edge.source === source && edge.target === target)) {
@@ -193,6 +196,7 @@ const hangleUpdateNodeData = (id: string, data: any) => {
 			...data
 		}
 	}
+	console.log('hangleUpdateNodeData', id, data)
 }
 
 /**

+ 42 - 12
packages/nodes/materials/http.ts

@@ -5,17 +5,47 @@
  * @LastEditTime: 2026-01-25 22:00:48
  * @Describe: HTTP请求节点
  */
-import type { IWorkflowNode} from '@repo/workflow'
+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: []
-    }
-};
+	id: 'http-node',
+	type: 'http-node',
+	label: 'HTTP',
+	position: { x: 468, y: 370 },
+	data: {
+		id: 'http-node-1',
+		label: 'HTTP',
+		description: 'http请求节点',
+		inputs: [],
+		outputs: [],
+		method: 'GET',
+		url: '',
+		headers: [],
+		params: [],
+		bodyType: 'json',
+		body: '',
+		verifySSL: true,
+		timeoutConfig: {
+			connect: 8,
+			read: 6,
+			write: 1
+		},
+		output: {
+			body: '',
+			status_code: 200,
+			headers: [],
+			files: []
+		},
+		errorConfig: {
+			retry: true,
+			max_retry: 3,
+			retry_delay: 100
+		},
+		exception: 'none',
+		exceptionDefaultValue: {
+			body: '',
+			status_code: 0,
+			headers: '{}'
+		}
+	}
+}

+ 47 - 0
packages/ui/components/input/Input.vue

@@ -0,0 +1,47 @@
+<template>
+	<el-input v-bind="$attrs" :class="[variant]" />
+</template>
+
+<script setup lang="ts">
+withDefaults(
+	defineProps<{
+		variant?: 'outlined' | 'borderless' | 'filled' | 'underline'
+	}>(),
+	{ variant: 'outlined' }
+)
+</script>
+
+<style lang="less" scoped>
+.borderless {
+	:deep(.el-input__wrapper) {
+		box-shadow: none;
+		&:hover,
+		&.is-focus {
+			box-shadow: 0 0 0 1px var(--el-input-focus-border-color) inset;
+		}
+	}
+}
+.underline {
+	:deep(.el-input__wrapper) {
+		box-shadow: none;
+		border-radius: 0;
+		border-bottom: solid 1px var(--el-input-border-color);
+		&:hover,
+		&.is-focus {
+			border-bottom: solid 1px var(--el-input-focus-border-color);
+		}
+	}
+}
+.filled {
+	:deep(.el-input__wrapper) {
+		box-shadow: none;
+		background-color: #f5f5f5;
+		&:hover {
+			background-color: #f0f0f0;
+		}
+		&.is-focus {
+			background-color: transparent;
+		}
+	}
+}
+</style>

+ 2 - 1
packages/ui/index.ts

@@ -2,5 +2,6 @@ import Icon from './components/icon/Icon.vue'
 import IconButton from './components/icon-button/IconButton.vue'
 import StickyNote from './components/sticky-note/StickyNote.vue'
 import Markdown from './components/markdown/Markdown.vue'
+import Input from './components/input/Input.vue'
 
-export { Icon, IconButton, StickyNote, Markdown }
+export { Icon, IconButton, StickyNote, Markdown, Input }

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

@@ -22,6 +22,7 @@ export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
 }
 export interface IWorkflowNode extends Node {
 	data: {
+		id: string
 		inputs: CanvasConnectionPort[]
 		outputs: CanvasConnectionPort[]
 		renderType?: 'default' | 'stickyNote' | 'custom'

+ 1 - 1
packages/workflow/src/Workflow.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="w-full h-full">
-		<Canvas :id="id" :nodes="nodes" :edges="edges" v-bind="$attrs" />
+		<Canvas :id="id" :nodes="nodes" :edges="edges" :read-only="readOnly" v-bind="$attrs" />
 		<slot />
 	</div>
 </template>

+ 28 - 9
packages/workflow/src/components/Canvas.vue

@@ -1,9 +1,15 @@
 <script lang="ts" setup>
-import type { IWorkflow, XYPosition, ConnectStartEvent, CanvasNodeMoveEvent } from '../Interface'
+import type {
+	IWorkflow,
+	XYPosition,
+	ConnectStartEvent,
+	CanvasNodeMoveEvent,
+	IWorkflowNode
+} from '../Interface'
 import type { SourceType } from '@repo/nodes'
 import type { NodeMouseEvent, Connection, NodeDragEvent } from '@vue-flow/core'
 
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, computed } from 'vue'
 import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
 import { MiniMap } from '@vue-flow/minimap'
 
@@ -88,6 +94,13 @@ const vueFlow = useVueFlow(props.id)
 
 const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vueFlow
 
+const nodeDataById = computed((): Record<string, IWorkflowNode['data']> => {
+	return props.nodes.reduce<Record<string, IWorkflowNode['data']>>((acc, node) => {
+		acc[node.id] = node.data as IWorkflowNode['data']
+		return acc
+	}, {})
+})
+
 /**
  * Returns the position of a mouse or touch event
  */
@@ -229,34 +242,40 @@ onMounted(() => {
 		@connect="onConnect"
 		@connect-start="onConnectStart"
 		@connect-end="onConnectEnd"
+		@nodes-initialized="onZoomToFit"
 		v-bind="$attrs"
 	>
 		<template #node-canvas-node="nodeProps">
-			<CanvasNode v-bind="nodeProps" @move="onUpdateNodePosition" @update="onUpdateNodeAttrs" />
+			<CanvasNode
+				v-bind="nodeProps"
+				:data="nodeDataById[nodeProps.id]!"
+				@move="onUpdateNodePosition"
+				@update="onUpdateNodeAttrs"
+			/>
 		</template>
 
 		<template #node-start-node="nodeProps">
-			<StartNode v-bind="nodeProps" />
+			<StartNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
 		</template>
 
 		<template #node-end-node="nodeProps">
-			<EndNode v-bind="nodeProps" />
+			<EndNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
 		</template>
 
 		<template #node-http-node="nodeProps">
-			<HttpNode v-bind="nodeProps" />
+			<HttpNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
 		</template>
 
 		<template #node-code-node="nodeProps">
-			<CodeNode v-bind="nodeProps" />
+			<CodeNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
 		</template>
 
 		<template #node-database-node="nodeProps">
-			<DataBaseNode v-bind="nodeProps" />
+			<DataBaseNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
 		</template>
 
 		<template #node-condition-node="nodeProps">
-			<ConditionNode v-bind="nodeProps" />
+			<ConditionNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
 		</template>
 
 		<template #edge-canvas-edge="edgeProps">

+ 10 - 24
packages/workflow/src/components/elements/node-temp/HttpNode1.vue

@@ -10,27 +10,16 @@ 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
-}
+import type { NodeProps } from '@vue-flow/core'
+import type { IWorkflowNode } from '../../../Interface'
 
-interface Props {
-	data: {
-		label?: string
-		description?: string
-		config?: HttpConfig
-		[key: string]: any
-	}
-	selected?: boolean
+type Props = NodeProps<IWorkflowNode['data']> & {
+	readOnly?: boolean
+	hovered?: boolean
 }
 
-const props = withDefaults(defineProps<Props>(), {
-	selected: false
-})
+const props = defineProps<Props>()
 
-console.log(props.data.id, '1212121')
 // 请求方法对应的颜色
 const methodColors: Record<string, string> = {
 	GET: '#1890ff',
@@ -39,9 +28,6 @@ const methodColors: Record<string, string> = {
 	DELETE: '#ff4d4f',
 	PATCH: '#722ed1'
 }
-
-const currentMethod = props.data.config?.method || 'GET'
-const methodColor = methodColors[currentMethod]
 </script>
 
 <template>
@@ -88,9 +74,9 @@ const methodColor = methodColors[currentMethod]
 				<!-- 请求方法标签 -->
 				<div
 					class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold text-white"
-					:style="{ backgroundColor: methodColor }"
+					:style="{ backgroundColor: methodColors[data.method || 'GET'] }"
 				>
-					{{ currentMethod }}
+					{{ data.method || 'GET' }}
 				</div>
 			</div>
 
@@ -102,7 +88,7 @@ const methodColor = methodColors[currentMethod]
 					<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 || '未配置' }}
+							{{ data?.url || '未配置' }}
 						</div>
 					</div>
 				</div>
@@ -112,7 +98,7 @@ const methodColor = methodColors[currentMethod]
 					<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>
+						<span class="font-medium ml-1">{{ data.timeoutConfig.connect }}ms</span>
 					</div>
 				</div>
 			</div>

+ 44 - 0
pnpm-lock.yaml

@@ -148,6 +148,9 @@ importers:
       element-plus:
         specifier: ^2.13.1
         version: 2.13.1(vue@3.5.27(typescript@5.9.3))
+      lodash-es:
+        specifier: ^4.17.21
+        version: 4.17.23
       monaco-editor:
         specifier: ^0.55.1
         version: 0.55.1
@@ -166,6 +169,9 @@ importers:
       vue-element-plus-x:
         specifier: ^1.3.98
         version: 1.3.98(rolldown@1.0.0-beta.50)(vue@3.5.27(typescript@5.9.3))
+      vue-hooks-plus:
+        specifier: ^2.4.1
+        version: 2.4.1(vue@3.5.27(typescript@5.9.3))
       vue-router:
         specifier: '4'
         version: 4.6.4(vue@3.5.27(typescript@5.9.3))
@@ -2552,6 +2558,9 @@ packages:
     peerDependencies:
       '@types/react': '*'
 
+  '@types/js-cookie@3.0.6':
+    resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
+
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
@@ -2961,6 +2970,9 @@ packages:
   '@vue/devtools-api@6.6.4':
     resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
 
+  '@vue/devtools-api@7.7.2':
+    resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==}
+
   '@vue/devtools-api@7.7.9':
     resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
 
@@ -4862,6 +4874,10 @@ packages:
   js-base64@2.6.4:
     resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
 
+  js-cookie@3.0.5:
+    resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+    engines: {node: '>=14'}
+
   js-tiktoken@1.0.21:
     resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
 
@@ -6229,6 +6245,10 @@ packages:
   scheduler@0.23.2:
     resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
 
+  screenfull@5.2.0:
+    resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
+    engines: {node: '>=0.10.0'}
+
   scroll-into-view-if-needed@2.2.31:
     resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
 
@@ -6942,6 +6962,11 @@ packages:
   vue-flow-layout@0.2.0:
     resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==}
 
+  vue-hooks-plus@2.4.1:
+    resolution: {integrity: sha512-DY9CW6U1ISeu10K3Hf7cKAvfG/S316rXt0Bt66BuTt9oplP+NuJYHtUuT/Ve5kWgNNEfyhrvguCY0JL0fBCzaw==}
+    peerDependencies:
+      vue: ^3.2.25
+
   vue-router@4.6.4:
     resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
     peerDependencies:
@@ -10062,6 +10087,8 @@ snapshots:
       '@types/react': 18.3.27
       hoist-non-react-statics: 3.3.2
 
+  '@types/js-cookie@3.0.6': {}
+
   '@types/json-schema@7.0.15': {}
 
   '@types/json5@0.0.29': {}
@@ -10649,6 +10676,10 @@ snapshots:
 
   '@vue/devtools-api@6.6.4': {}
 
+  '@vue/devtools-api@7.7.2':
+    dependencies:
+      '@vue/devtools-kit': 7.7.9
+
   '@vue/devtools-api@7.7.9':
     dependencies:
       '@vue/devtools-kit': 7.7.9
@@ -12933,6 +12964,8 @@ snapshots:
 
   js-base64@2.6.4: {}
 
+  js-cookie@3.0.5: {}
+
   js-tiktoken@1.0.21:
     dependencies:
       base64-js: 1.5.1
@@ -14674,6 +14707,8 @@ snapshots:
     dependencies:
       loose-envify: 1.4.0
 
+  screenfull@5.2.0: {}
+
   scroll-into-view-if-needed@2.2.31:
     dependencies:
       compute-scroll-into-view: 1.0.20
@@ -15620,6 +15655,15 @@ snapshots:
 
   vue-flow-layout@0.2.0: {}
 
+  vue-hooks-plus@2.4.1(vue@3.5.27(typescript@5.9.3)):
+    dependencies:
+      '@types/js-cookie': 3.0.6
+      '@vue/devtools-api': 7.7.2
+      js-cookie: 3.0.5
+      lodash-es: 4.17.23
+      screenfull: 5.2.0
+      vue: 3.5.27(typescript@5.9.3)
+
   vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)):
     dependencies:
       '@vue/devtools-api': 6.6.4