jiaxing.liao пре 1 недеља
родитељ
комит
81a57e21ce

+ 25 - 1
apps/web/src/nodes/src/index.ts

@@ -16,6 +16,28 @@ import { loopNode } from './loop'
 import { listNode } from './list'
 import type { INodeType } from '../Interface'
 
+const loopStartNode = {
+	...startNode,
+	name: 'custom-loop-start',
+	nodeType: 'custom-loop-start',
+	displayName: '循环开始',
+	schema: {
+		...startNode.schema,
+		nodeType: 'custom-loop-start'
+	}
+}
+
+const iterationStartNode = {
+	...startNode,
+	name: 'custom-iteration-start',
+	nodeType: 'custom-iteration-start',
+	displayName: '迭代开始',
+	schema: {
+		...startNode.schema,
+		nodeType: 'custom-iteration-start'
+	}
+}
+
 const nodes = [
 	startNode,
 	endNode,
@@ -25,7 +47,9 @@ const nodes = [
 	codeNode,
 	iterationNode,
 	loopNode,
-	listNode
+	listNode,
+	loopStartNode,
+	iterationStartNode
 ]
 
 const nodeMap = nodes.reduce(

+ 2 - 2
apps/web/src/nodes/src/iteration/index.ts

@@ -61,8 +61,8 @@ export const iterationNode: INodeType = {
 			x: 20,
 			y: 30
 		},
-		width: 96,
-		height: 96,
+		width: 424,
+		height: 244,
 		selected: false,
 		nodeType: 'iteration',
 		zIndex: 1,

+ 2 - 2
apps/web/src/nodes/src/loop/index.ts

@@ -74,8 +74,8 @@ export const loopNode: INodeType = {
 			x: 20,
 			y: 30
 		},
-		width: 96,
-		height: 96,
+		width: 424,
+		height: 244,
 		selected: false,
 		nodeType: 'loop',
 		zIndex: 1,

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

@@ -470,6 +470,7 @@ const handleNodeCreate = (value: { type: string; position?: XYPosition } | strin
 	if (typeof value === 'string') {
 		if (value === 'stickyNote') {
 			workflow.value?.nodes.push({
+				appAgentId: workflow.value.id,
 				type: 'canvas-node',
 				zIndex: -1,
 				nodeType: 'stickyNote',

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

@@ -21,9 +21,11 @@ export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
 	offset?: { top?: string; left?: string }
 }
 export interface IWorkflowNode extends Node {
+	appAgentId: string
 	name?: string
 	remark?: string
 	nodeType: string
+	parentId?: string
 	data: {
 		id: string
 		// 位置
@@ -32,8 +34,6 @@ export interface IWorkflowNode extends Node {
 		width: number
 		// 高度
 		height: number
-		// 父级节点
-		parentId?: string
 		// 节点层级
 		zIndex?: number
 		// 节点类型

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

@@ -7,6 +7,7 @@
 			:edges="edges"
 			:read-only="readOnly"
 			:node-map="nodeMap"
+			hide-child-node
 			v-bind="$attrs"
 		/>
 		<slot />

+ 16 - 3
packages/workflow/src/components/Canvas.vue

@@ -76,17 +76,27 @@ const props = withDefaults(
 		id?: string
 		nodes: IWorkflow['nodes']
 		edges: IWorkflow['edges']
+		// 是否隐藏带parentId的节点
+		hideChildNode?: boolean
 		readOnly?: boolean
 		nodeMap: Record<string, any>
+		showControlBar?: boolean
+		zoomToFit?: boolean
 	}>(),
 	{
 		id: 'canvas',
 		readOnly: false,
 		nodes: () => [],
-		edges: () => []
+		edges: () => [],
+		showControlBar: true,
+		zoomToFit: true
 	}
 )
 
+const getNodes = computed(() =>
+	props.hideChildNode ? props.nodes.filter((node) => !node?.parentId) : props.nodes
+)
+
 const showMinimap = ref(false)
 const vueFlow = useVueFlow(props.id)
 
@@ -299,7 +309,8 @@ function onRunNode(id: string) {
 let loaded = false
 function onNodesInitialized() {
 	if (!loaded) {
-		onZoomToFit()
+		props.zoomToFit && onZoomToFit()
+		onResetZoom()
 		loaded = true
 		emit('initialized:nodes')
 	}
@@ -325,7 +336,7 @@ defineExpose({
 <template>
 	<VueFlow
 		:id="id"
-		:nodes="nodes"
+		:nodes="getNodes"
 		:edges="edges"
 		:connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
 		:connection-radius="60"
@@ -352,6 +363,7 @@ defineExpose({
 					:hovered="nodesHoveredById[nodeProps.id]"
 					@move="onUpdateNodePosition"
 					@update="onUpdateNodeAttrs"
+					@add-inner-node="emit('click:node:add', nodeProps.id, 'inner')"
 					@delete="onDeleteNode"
 					@run="onRunNode"
 				/>
@@ -388,6 +400,7 @@ defineExpose({
 		/>
 
 		<CanvasControlBar
+			v-if="showControlBar"
 			@zoom-in="onZoomIn"
 			@zoom-out="onZoomOut"
 			@zoom-to-fit="onZoomToFit"

+ 3 - 2
packages/workflow/src/components/elements/nodes/CanvasNode.vue

@@ -27,6 +27,7 @@ const emit = defineEmits<{
 	move: [id: string, position: { x: number; y: number }]
 	delete: [id: string]
 	run: [id: string]
+	'add-inner-node': []
 }>()
 
 /**
@@ -115,7 +116,7 @@ provide('canvas-node-data', {
 
 <template>
 	<div class="relative">
-		<NodeRenderer v-bind="$attrs" @update="onUpdate" />
+		<NodeRenderer v-bind="$attrs" @update="onUpdate" @add-inner-node="emit('add-inner-node')" />
 
 		<template v-for="target in inputs" :key="'handle-inputs-port' + target.index">
 			<CanvasHandle v-bind="target" type="target" />
@@ -125,6 +126,6 @@ provide('canvas-node-data', {
 			<CanvasHandle v-bind="source" type="source" />
 		</template>
 
-		<CanvasNodeToolBar @delete="onDelete" @run="onRun" />
+		<CanvasNodeToolBar @delete="onDelete" @run="onRun" :hovered="hovered" />
 	</div>
 </template>

+ 5 - 1
packages/workflow/src/components/elements/nodes/CanvasNodeToolBar.vue

@@ -11,6 +11,10 @@ const emit = defineEmits<{
 	run: []
 }>()
 
+const props = defineProps<{
+	hovered?: boolean
+}>()
+
 const node = inject<{
 	props?: NodeProps<IWorkflowNode['data']> & {
 		readOnly?: boolean
@@ -42,7 +46,7 @@ const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(nu
 const delayedHoveredTimeout = 600
 
 watch(
-	() => node?.props?.hovered,
+	() => props?.hovered,
 	(isHovered) => {
 		if (isHovered) {
 			if (delayedHoveredSetTimeoutRef.value) clearTimeout(delayedHoveredSetTimeoutRef.value)

+ 2 - 3
packages/workflow/src/components/elements/nodes/render-types/NodeDefault.vue

@@ -31,8 +31,7 @@ const nodeClass = computed(() => {
 
 	return classes
 })
-
-const nodeData = computed(() => node?.props?.node)
+const nodeData = computed(() => node?.props?.data)
 
 const nodeType = computed(() => nodeMap?.[nodeData.value?.nodeType!])
 
@@ -45,7 +44,7 @@ const warningInfo = computed(() => {
 <template>
 	<div
 		:class="nodeClass"
-		class="default-node w-96px h-96px bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-8px relative"
+		class="default-node bg-#fff box-border border-2 border-solid border-#dcdcdc rounded-8px relative"
 	>
 		<div className="w-full h-full relative flex items-center justify-center">
 			<div

+ 127 - 0
packages/workflow/src/components/elements/nodes/render-types/NodeLoop.vue

@@ -0,0 +1,127 @@
+<script setup lang="ts">
+import { inject, computed } from 'vue'
+import { Icon } from '@repo/ui'
+import { NodeResizer } from '@vue-flow/node-resizer'
+import type { OnResize } from '@vue-flow/node-resizer'
+import Canvas from '../../../Canvas.vue'
+
+import type { NodeProps, XYPosition } from '@vue-flow/core'
+import type { IWorkflowNode, CanvasConnectionPort, IWorkflowEdge } from '../../../../Interface'
+
+import '@vue-flow/node-resizer/dist/style.css'
+
+defineOptions({
+	inheritAttrs: false
+})
+
+const node = inject<{
+	props?: NodeProps<IWorkflowNode> & {
+		readOnly?: boolean
+		hovered?: boolean
+		node: IWorkflowNode
+	}
+	inputs?: { value: CanvasConnectionPort[] }
+	outputs?: { value: CanvasConnectionPort[] }
+}>('canvas-node-data')
+
+const nodeMap = inject<{ nodeMap: Record<string, any> }>('vueflow')?.nodeMap
+
+const nodes = inject<{ nodes: IWorkflowNode[] }>('vueflow')?.nodes
+const edges = inject<{ edges: IWorkflowEdge[] }>('vueflow')?.edges
+
+const childrenNodes = computed(
+	() => nodes?.filter((item) => item?.parentId === node?.props?.id) || []
+)
+console.log(childrenNodes.value, nodes)
+const emit = defineEmits<{
+	update: [parameters: Record<string, unknown>]
+	move: [position: XYPosition]
+	'add-inner-node': []
+}>()
+
+const nodeData = computed(() => node?.props?.node?.data ?? node?.props?.data)
+const nodeType = computed(() => {
+	const type = nodeData.value?.nodeType
+	return type ? nodeMap?.[type] : undefined
+})
+const isReadOnly = computed(() => node?.props?.readOnly ?? false)
+
+const width = computed(() => Number(nodeData.value?.width) || 424)
+const height = computed(() => Number(nodeData.value?.height) || 244)
+
+const nodeClass = computed(() => {
+	const classes: string[] = []
+	if (node?.props?.selected) {
+		classes.push('ring-2', 'ring-#06aed4')
+	}
+	return classes
+})
+
+function onResize(event: OnResize) {
+	emit('move', {
+		x: event.params.x,
+		y: event.params.y
+	})
+	emit('update', {
+		...(event.params.width != null ? { width: event.params.width } : {}),
+		...(event.params.height != null ? { height: event.params.height } : {})
+	})
+}
+
+function onAddNode() {
+	if (!isReadOnly.value) {
+		emit('add-inner-node')
+	}
+}
+</script>
+
+<template>
+	<NodeResizer
+		:min-width="280"
+		:min-height="180"
+		:width="width"
+		:height="height"
+		:is-visible="!isReadOnly && node?.props?.selected"
+		handle-class-name="bg-#06aed4! border-white! w-8px h-8px"
+		line-class-name="border-#06aed4!"
+		@resize="onResize"
+	/>
+
+	<div
+		:class="nodeClass"
+		class="node-loop rounded-12px border-2 border-solid border-#dcdcdc bg-#fafafa overflow-hidden flex flex-col"
+		:style="{ width: width + 'px', height: height + 'px' }"
+	>
+		<!-- 标题栏 -->
+		<div
+			class="loop-header shrink-0 flex items-center gap-8px pl-12px py-8px border-b border-b-solid border-#e8e8e8 bg-#fff"
+		>
+			<div
+				class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-4px"
+				:style="{ background: nodeType?.iconColor ?? '#06aed4' }"
+			>
+				<Icon :icon="nodeType?.icon ?? 'lucide:infinity'" color="#ffffff" :size="16" />
+			</div>
+			<span class="text-14px font-medium text-#333">{{ nodeType?.displayName ?? '循环' }}</span>
+		</div>
+
+		<!-- 内部画布区域:虚线网格 + 占位内容 -->
+		<div class="loop-body flex-1 min-h-0 relative p-16px">
+			<div
+				class="absolute inset-16px rounded-8px border-1 border-dashed border-#d9d9d9 bg-[repeating-linear-gradient(to_right,#e8e8e8_0,transparent_1px),repeating-linear-gradient(to_bottom,#e8e8e8_0,transparent_1px)] bg-[length:12px_12px]"
+			/>
+			<div class="w-full h-full relative z-1 flex items-center gap-12px pt-8px">
+				<Canvas
+					:id="node?.props?.id"
+					:nodes="childrenNodes"
+					:edges="edges || []"
+					:read-only="isReadOnly"
+					:node-map="nodeMap!"
+					:show-control-bar="false"
+					:hide-child-node="false"
+					:zoom-to-fit="false"
+				/>
+			</div>
+		</div>
+	</div>
+</template>

+ 10 - 0
packages/workflow/src/components/elements/nodes/render-types/NodeRenderer.vue

@@ -2,6 +2,7 @@
 import { inject, computed, type Ref } from 'vue'
 import NodeDefault from './NodeDefault.vue'
 import NodeStickyNote from './NodeStickyNote.vue'
+import NodeLoop from './NodeLoop.vue'
 
 import type { NodeProps } from '@vue-flow/core'
 import type { IWorkflowNode, CanvasConnectionPort } from '../../../../Interface'
@@ -13,9 +14,18 @@ const node = inject<{
 }>('canvas-node-data')
 
 const nodeType = computed(() => node?.props?.data?.nodeType)
+
+defineEmits<{
+	'add-inner-node': []
+}>()
 </script>
 
 <template>
 	<NodeStickyNote v-if="nodeType === 'stickyNote'" v-bind="$attrs" />
+	<NodeLoop
+		v-else-if="nodeType === 'loop' || nodeType === 'iteration'"
+		v-bind="$attrs"
+		@add-inner-node="$emit('add-inner-node')"
+	/>
 	<NodeDefault v-else v-bind="$attrs"> </NodeDefault>
 </template>