diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 56639692..63aa1aa1 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,9 +1,14 @@ -import { memo } from "react"; +import { memo, useRef, useState } from "react"; import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; +import ConditionNodeConfigForm, { ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; +import { WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { produce } from "immer"; +import { useWorkflowStore } from "@/stores/workflow"; +import { useZustandShallowSelector } from "@/hooks"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; @@ -11,7 +16,37 @@ export type ConditionNodeProps = SharedNodeProps & { }; const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - // TODO: 条件分支 + const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); + + const [formPending, setFormPending] = useState(false); + const formRef = useRef(null); + + const [drawerOpen, setDrawerOpen] = useState(false); + + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForCondition; + + const handleDrawerConfirm = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + const newValues = getFormValues(); + const newNode = produce(node, (draft) => { + draft.config = { + ...newValues, + }; + draft.validated = true; + }); + await updateNode(newNode); + } finally { + setFormPending(false); + } + }; return ( <> @@ -30,7 +65,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP } placement="rightTop" > - + setDrawerOpen(true)}>
+ + setDrawerOpen(open)} + getFormValues={() => formRef.current!.getFieldsValue()} + > + + @@ -47,3 +93,4 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP }; export default memo(ConditionNode); + diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx new file mode 100644 index 00000000..4eca1617 --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -0,0 +1,250 @@ +import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; +import { Button, Card, Form, Input, Select, Space, Radio } from "antd"; +import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; + +import { + WorkflowNodeConfigForCondition, + Expr, + WorkflowNodeIOValueSelector, + ComparisonOperator, + LogicalOperator, + isConstExpr, + isVarExpr, + WorkflowNode, +} from "@/domain/workflow"; +import { FormInstance } from "antd"; +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +// 表单内部使用的扁平结构 - 修改后只保留必要字段 +interface ConditionItem { + leftSelector: WorkflowNodeIOValueSelector; + operator: ComparisonOperator; + rightValue: string; +} + +type ConditionNodeConfigFormFieldValues = { + conditions: ConditionItem[]; + logicalOperator: LogicalOperator; +}; + +export type ConditionNodeConfigFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + initialValues?: Partial; + onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; + availableSelectors?: WorkflowNodeIOValueSelector[]; + nodeId: string; +}; + +export type ConditionNodeConfigFormInstance = { + getFieldsValue: () => ReturnType["getFieldsValue"]>; + resetFields: FormInstance["resetFields"]; + validateFields: FormInstance["validateFields"]; +}; + +// 初始表单值 +const initFormModel = (): ConditionNodeConfigFormFieldValues => { + return { + conditions: [ + { + leftSelector: undefined as unknown as WorkflowNodeIOValueSelector, + operator: "==", + rightValue: "", + }, + ], + logicalOperator: "and", + }; +}; + +// 将表单值转换为表达式结构 +const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { + // 创建单个条件的表达式 + const createComparisonExpr = (condition: ConditionItem): Expr => { + const left: Expr = { type: "var", selector: condition.leftSelector }; + const right: Expr = { type: "const", value: condition.rightValue || "" }; + + return { + type: "compare", + op: condition.operator, + left, + right, + }; + }; + + // 如果只有一个条件,直接返回比较表达式 + if (values.conditions.length === 1) { + return createComparisonExpr(values.conditions[0]); + } + + // 多个条件,通过逻辑运算符连接 + let expr: Expr = createComparisonExpr(values.conditions[0]); + + for (let i = 1; i < values.conditions.length; i++) { + expr = { + type: "logical", + op: values.logicalOperator, + left: expr, + right: createComparisonExpr(values.conditions[i]), + }; + } + + return expr; +}; + +// 递归提取表达式中的条件项 +const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { + if (!expr) return initFormModel(); + + const conditions: ConditionItem[] = []; + let logicalOp: LogicalOperator = "and"; + + const extractComparisons = (expr: Expr): void => { + if (expr.type === "compare") { + // 确保左侧是变量,右侧是常量 + if (isVarExpr(expr.left) && isConstExpr(expr.right)) { + conditions.push({ + leftSelector: expr.left.selector, + operator: expr.op, + rightValue: String(expr.right.value), + }); + } + } else if (expr.type === "logical") { + logicalOp = expr.op; + extractComparisons(expr.left); + extractComparisons(expr.right); + } + }; + + extractComparisons(expr); + + return { + conditions: conditions.length > 0 ? conditions : initFormModel().conditions, + logicalOperator: logicalOp, + }; +}; + +const ConditionNodeConfigForm = forwardRef( + ({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => { + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + + const [form] = Form.useForm(); + const [formModel, setFormModel] = useState(initFormModel()); + + const [previousNodes, setPreviousNodes] = useState([]); + useEffect(() => { + const previousNodes = getWorkflowOuptutBeforeId(nodeId); + setPreviousNodes(previousNodes); + }, [nodeId]); + + // 初始化表单值 + useEffect(() => { + if (initialValues?.expression) { + const formValues = expressionToForm(initialValues.expression); + form.setFieldsValue(formValues); + setFormModel(formValues); + } + }, [form, initialValues]); + + // 公开表单方法 + useImperativeHandle( + ref, + () => ({ + getFieldsValue: form.getFieldsValue, + resetFields: form.resetFields, + validateFields: form.validateFields, + }), + [form] + ); + + // 表单值变更处理 + const handleFormChange = (changedValues: any, values: ConditionNodeConfigFormFieldValues) => { + setFormModel(values); + + // 转换为表达式结构并通知父组件 + const expression = formToExpression(values); + onValuesChange?.({ expression }); + }; + + return ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + 1 ? + + + )} + + + {formModel.conditions && formModel.conditions.length > 1 && ( + + + 满足所有条件 (AND) + 满足任一条件 (OR) + + + )} +
+ ); + } +); + +export default memo(ConditionNodeConfigForm); + diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 06226425..762d57ab 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -165,6 +165,10 @@ export type WorkflowNodeConfigForNotify = { providerConfig?: Record; }; +export type WorkflowNodeConfigForCondition = { + expression: Expr; +}; + export type WorkflowNodeConfigForBranch = never; export type WorkflowNodeConfigForEnd = never; @@ -185,6 +189,32 @@ export type WorkflowNodeIOValueSelector = { // #endregion +// #region Condition expression + +type Value = string | number | boolean; + +export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!="; + +export type LogicalOperator = "and" | "or" | "not"; + +export type ConstExpr = { type: "const"; value: Value }; +export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector }; +export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr }; +export type NotExpr = { type: "not"; expr: Expr }; + +export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; + +export const isConstExpr = (expr: Expr): expr is ConstExpr => { + return expr.type === "const"; +}; + +export const isVarExpr = (expr: Expr): expr is VarExpr => { + return expr.type === "var"; +}; + +// #endregion + const isBranchLike = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch; }; @@ -433,7 +463,17 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => { +const typeEqual = (a: WorkflowNodeIO, t: string) => { + if (t === "all") { + return true; + } + if (a.type === t) { + return true; + } + return false; +}; + +export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string = "all"): WorkflowNode[] => { // 某个分支的节点,不应该能获取到相邻分支上节点的输出 const outputs: WorkflowNode[] = []; @@ -445,10 +485,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: return true; } - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => io.type === type)) { + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => typeEqual(io, type))) { output.push({ ...current, - outputs: current.outputs.filter((io) => io.type === type), + outputs: current.outputs.filter((io) => typeEqual(io, type)), }); } @@ -501,3 +541,4 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => { return true; }; + diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 8057fda5..d20fec16 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -32,7 +32,7 @@ export type WorkflowState = { addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, type?: string) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => { + getWorkflowOuptutBeforeId: (nodeId: string, type: string = "all") => { return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type); }, }));