diff --git a/ui/src/workflow-canvas/icons/loop-body-node-icon.vue b/ui/src/workflow-canvas/icons/loop-body-node-icon.vue new file mode 100644 index 00000000000..b73cf6b4daa --- /dev/null +++ b/ui/src/workflow-canvas/icons/loop-body-node-icon.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/workflow-canvas/icons/loop-break-node-icon.vue b/ui/src/workflow-canvas/icons/loop-break-node-icon.vue new file mode 100644 index 00000000000..b73cf6b4daa --- /dev/null +++ b/ui/src/workflow-canvas/icons/loop-break-node-icon.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/workflow-canvas/icons/loop-continue-node-icon.vue b/ui/src/workflow-canvas/icons/loop-continue-node-icon.vue new file mode 100644 index 00000000000..b73cf6b4daa --- /dev/null +++ b/ui/src/workflow-canvas/icons/loop-continue-node-icon.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/workflow-canvas/icons/loop-node-icon.vue b/ui/src/workflow-canvas/icons/loop-node-icon.vue new file mode 100644 index 00000000000..b73cf6b4daa --- /dev/null +++ b/ui/src/workflow-canvas/icons/loop-node-icon.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/workflow-canvas/icons/loop-start-node-icon.vue b/ui/src/workflow-canvas/icons/loop-start-node-icon.vue new file mode 100644 index 00000000000..b73cf6b4daa --- /dev/null +++ b/ui/src/workflow-canvas/icons/loop-start-node-icon.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/src/workflow-canvas/index.vue b/ui/src/workflow-canvas/index.vue index bc28f34501f..1652efdd818 100644 --- a/ui/src/workflow-canvas/index.vue +++ b/ui/src/workflow-canvas/index.vue @@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash' import LogicFlow, { type GraphModel } from '@logicflow/core' import { SelectionSelect } from '@logicflow/extension' import AppEdge from '@/workflow-canvas/core/edge/index' +import LoopEdge from '@/workflow-canvas/core/edge/loop-edge' import { initDefaultShortcut } from '@/workflow-canvas/core/shortcut' import { disconnectAll, getTeleport } from '@/workflow-canvas/core/teleport' import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node' @@ -66,7 +67,7 @@ function renderGraphData(data: LogicFlow.GraphConfigData = props.data ?? {}) { initDefaultShortcut(lf.value, lf.value.graphModel) // 节点按需读取页面传入的已保存配置,保持与保存响应一致。 lf.value.graphModel.getDefaultModelSettings = () => props.defaultModelSettings - lf.value.batchRegister([...Object.values(nodeModules).map(({ default: node }) => node), AppEdge]) + lf.value.batchRegister([...Object.values(nodeModules).map(({ default: node }) => node), AppEdge, LoopEdge]) lf.value.setDefaultEdgeType('app-edge') lf.value.render(data ? data : {}) diff --git a/ui/src/workflow-canvas/node-menu/index.vue b/ui/src/workflow-canvas/node-menu/index.vue index b91ac07e9fc..49d0531502d 100644 --- a/ui/src/workflow-canvas/node-menu/index.vue +++ b/ui/src/workflow-canvas/node-menu/index.vue @@ -35,7 +35,7 @@ function handleDragStart(node: NodeMenuItem, event: PointerEvent) { - + diff --git a/ui/src/workflow-canvas/nodes/loop-body-node/LoopBodyContainer.vue b/ui/src/workflow-canvas/nodes/loop-body-node/LoopBodyContainer.vue new file mode 100644 index 00000000000..c725ebf790f --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-body-node/LoopBodyContainer.vue @@ -0,0 +1,76 @@ + + + + + + + {{ model.properties.stepName }} + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/workflow-canvas/nodes/loop-body-node/index.ts b/ui/src/workflow-canvas/nodes/loop-body-node/index.ts new file mode 100644 index 00000000000..366273dc34e --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-body-node/index.ts @@ -0,0 +1,29 @@ +import LoopBodyNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType } from '@/workflow-canvas/types' +import type { Model } from '@logicflow/core' + +class LoopBodyModel extends WorkflowNodeModel { + setLoopBody?: () => void + loopLayout?: () => void + + getDefaultAnchor(): Model.AnchorConfig[] { + return [{ x: this.x, y: this.y - this.height / 2 + 10, id: `${this.id}_children`, type: 'children', edgeAddable: false }] + } + + refreshBranch() { + ;[...this.incoming.edges, ...this.outgoing.edges].forEach((edge) => (edge as { updatePathByAnchor?: () => void }).updatePathByAnchor?.()) + } +} + +class LoopBodyNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, LoopBodyNodeVue) + } +} + +export default { + type: WorkflowNodeType.LoopBodyNode, + model: LoopBodyModel, + view: LoopBodyNodeView, +} diff --git a/ui/src/workflow-canvas/nodes/loop-body-node/index.vue b/ui/src/workflow-canvas/nodes/loop-body-node/index.vue new file mode 100644 index 00000000000..c9c9adc7ef3 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-body-node/index.vue @@ -0,0 +1,190 @@ + + + + + + diff --git a/ui/src/workflow-canvas/nodes/loop-break-node/index.ts b/ui/src/workflow-canvas/nodes/loop-break-node/index.ts new file mode 100644 index 00000000000..c42ad5eeea2 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-break-node/index.ts @@ -0,0 +1,15 @@ +import LoopBreakNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType } from '@/workflow-canvas/types' + +class LoopBreakNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, LoopBreakNodeVue) + } +} + +export default { + type: WorkflowNodeType.LoopBreakNode, + model: WorkflowNodeModel, + view: LoopBreakNodeView, +} diff --git a/ui/src/workflow-canvas/nodes/loop-break-node/index.vue b/ui/src/workflow-canvas/nodes/loop-break-node/index.vue new file mode 100644 index 00000000000..32f08bd497d --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-break-node/index.vue @@ -0,0 +1,21 @@ + + + 终止当前循环,跳出循环体 + + + diff --git a/ui/src/workflow-canvas/nodes/loop-continue-node/index.ts b/ui/src/workflow-canvas/nodes/loop-continue-node/index.ts new file mode 100644 index 00000000000..2846b3b8616 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-continue-node/index.ts @@ -0,0 +1,15 @@ +import LoopContinueNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType } from '@/workflow-canvas/types' + +class LoopContinueNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, LoopContinueNodeVue) + } +} + +export default { + type: WorkflowNodeType.LoopContinueNode, + model: WorkflowNodeModel, + view: LoopContinueNodeView, +} diff --git a/ui/src/workflow-canvas/nodes/loop-continue-node/index.vue b/ui/src/workflow-canvas/nodes/loop-continue-node/index.vue new file mode 100644 index 00000000000..1fbeac62b57 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-continue-node/index.vue @@ -0,0 +1,21 @@ + + + 结束当前迭代,进入下一次循环 + + + diff --git a/ui/src/workflow-canvas/nodes/loop-node/index.ts b/ui/src/workflow-canvas/nodes/loop-node/index.ts new file mode 100644 index 00000000000..5dcb664435b --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-node/index.ts @@ -0,0 +1,28 @@ +import LoopNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType } from '@/workflow-canvas/types' +import type { Model } from '@logicflow/core' + +class LoopNodeModel extends WorkflowNodeModel { + getDefaultAnchor(): Model.AnchorConfig[] { + const anchors = super.getDefaultAnchor() + anchors.push({ x: this.x, y: this.y + this.height / 2 - 25, id: `${this.id}_children`, type: 'children', edgeAddable: false }) + return anchors + } + + refreshBranch() { + ;[...this.incoming.edges, ...this.outgoing.edges].forEach((edge) => (edge as { updatePathByAnchor?: () => void }).updatePathByAnchor?.()) + } +} + +class LoopNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, LoopNodeVue) + } +} + +export default { + type: WorkflowNodeType.LoopNode, + model: LoopNodeModel, + view: LoopNodeView, +} diff --git a/ui/src/workflow-canvas/nodes/loop-node/index.vue b/ui/src/workflow-canvas/nodes/loop-node/index.vue new file mode 100644 index 00000000000..27ae686161f --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-node/index.vue @@ -0,0 +1,138 @@ + + + 节点设置 + + + + + + + + + + + + + + + + + + diff --git a/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldDialog.vue b/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldDialog.vue new file mode 100644 index 00000000000..f7c8b1295af --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldDialog.vue @@ -0,0 +1,76 @@ + + + + + + + + + + + + + 取消 + {{ isEdit ? '保存' : '添加' }} + + + + + diff --git a/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldTable.vue b/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldTable.vue new file mode 100644 index 00000000000..fbd0b24c2a5 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-start-node/component/LoopFieldTable.vue @@ -0,0 +1,95 @@ + + + + 循环变量 + + + 添加 + + + + + + {{ row.field }} + + + + + {{ row.label }} + + + + + + + + + + + + + + + + + diff --git a/ui/src/workflow-canvas/nodes/loop-start-node/index.ts b/ui/src/workflow-canvas/nodes/loop-start-node/index.ts new file mode 100644 index 00000000000..0bdefed16d3 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-start-node/index.ts @@ -0,0 +1,33 @@ +import LoopStartNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType, type WorkflowNodeField } from '@/workflow-canvas/types' + +interface LoopStartNodeProperties { + config?: { fields?: Array<{ label: string; value: string }>; globalFields?: unknown[] } + loop_input_field_list?: Array<{ field?: string; label?: string; variable?: string }> + [key: string]: unknown +} + +class LoopStartNodeModel extends WorkflowNodeModel { + override getNodeFieldList(): WorkflowNodeField[] { + const properties = this.properties as LoopStartNodeProperties + const inputFields = + properties.loop_input_field_list && properties.loop_input_field_list.length + ? properties.loop_input_field_list.map((item) => ({ field: item.field ?? item.variable ?? '', label: item.label ?? '' })) + : (properties.config?.fields ?? []).map((field) => ({ field: field.value, label: field.label })) + const loopFields = inputFields.map((item) => ({ label: item.label, value: item.field })).filter((field) => Boolean(field.value)) + return [{ value: 'loop', label: '循环变量', type: 'loop', children: loopFields }, ...super.getNodeFieldList()] + } +} + +class LoopStartNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, LoopStartNodeVue) + } +} + +export default { + type: WorkflowNodeType.LoopStartNode, + model: LoopStartNodeModel, + view: LoopStartNodeView, +} diff --git a/ui/src/workflow-canvas/nodes/loop-start-node/index.vue b/ui/src/workflow-canvas/nodes/loop-start-node/index.vue new file mode 100644 index 00000000000..0f3bf255068 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/loop-start-node/index.vue @@ -0,0 +1,45 @@ + + + 节点设置 + + + 循环变量 + + + + {{ item.label }} {{ '{' + item.value + '}' }} + + + + + + + + + + + +