mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 13:39:53 +00:00
improve ui
This commit is contained in:
parent
2906576de0
commit
b8ab077b57
@ -1,9 +1,14 @@
|
|||||||
import { memo } from "react";
|
import { memo, useRef, useState } from "react";
|
||||||
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
|
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
|
||||||
import { Button, Card, Popover } from "antd";
|
import { Button, Card, Popover } from "antd";
|
||||||
|
|
||||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||||
import AddNode from "./AddNode";
|
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 & {
|
export type ConditionNodeProps = SharedNodeProps & {
|
||||||
branchId: string;
|
branchId: string;
|
||||||
@ -11,7 +16,37 @@ export type ConditionNodeProps = SharedNodeProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
||||||
// TODO: 条件分支
|
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
|
||||||
|
|
||||||
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
const formRef = useRef<ConditionNodeConfigFormInstance>(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,7 +65,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
|||||||
}
|
}
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
>
|
>
|
||||||
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable onClick={() => setDrawerOpen(true)}>
|
||||||
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
||||||
<SharedNode.Title
|
<SharedNode.Title
|
||||||
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
||||||
@ -39,6 +74,17 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<SharedNode.ConfigDrawer
|
||||||
|
node={node}
|
||||||
|
open={drawerOpen}
|
||||||
|
pending={formPending}
|
||||||
|
onConfirm={handleDrawerConfirm}
|
||||||
|
onOpenChange={(open) => setDrawerOpen(open)}
|
||||||
|
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||||
|
>
|
||||||
|
<ConditionNodeConfigForm nodeId={node.id} ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||||
|
</SharedNode.ConfigDrawer>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<AddNode node={node} disabled={disabled} />
|
<AddNode node={node} disabled={disabled} />
|
||||||
@ -47,3 +93,4 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ConditionNode);
|
export default memo(ConditionNode);
|
||||||
|
|
||||||
|
250
ui/src/components/workflow/node/ConditionNodeConfigForm.tsx
Normal file
250
ui/src/components/workflow/node/ConditionNodeConfigForm.tsx
Normal file
@ -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<WorkflowNodeConfigForCondition>;
|
||||||
|
onValuesChange?: (values: WorkflowNodeConfigForCondition) => void;
|
||||||
|
availableSelectors?: WorkflowNodeIOValueSelector[];
|
||||||
|
nodeId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConditionNodeConfigFormInstance = {
|
||||||
|
getFieldsValue: () => ReturnType<FormInstance<ConditionNodeConfigFormFieldValues>["getFieldsValue"]>;
|
||||||
|
resetFields: FormInstance<ConditionNodeConfigFormFieldValues>["resetFields"];
|
||||||
|
validateFields: FormInstance<ConditionNodeConfigFormFieldValues>["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<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
||||||
|
({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => {
|
||||||
|
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||||
|
|
||||||
|
const [form] = Form.useForm<ConditionNodeConfigFormFieldValues>();
|
||||||
|
const [formModel, setFormModel] = useState<ConditionNodeConfigFormFieldValues>(initFormModel());
|
||||||
|
|
||||||
|
const [previousNodes, setPreviousNodes] = useState<WorkflowNode[]>([]);
|
||||||
|
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 (
|
||||||
|
<Form form={form} className={className} style={style} layout="vertical" disabled={disabled} initialValues={formModel} onValuesChange={handleFormChange}>
|
||||||
|
<Form.List name="conditions">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Card
|
||||||
|
key={key}
|
||||||
|
size="small"
|
||||||
|
className="mb-3"
|
||||||
|
extra={fields.length > 1 ? <Button icon={<DeleteOutlined />} danger type="text" onClick={() => remove(name)} /> : null}
|
||||||
|
>
|
||||||
|
{/* 将三个表单项放在一行 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 左侧变量选择器 */}
|
||||||
|
<Form.Item {...restField} name={[name, "leftSelector"]} className="mb-0 flex-1" rules={[{ required: true, message: "请选择变量" }]}>
|
||||||
|
<Select placeholder="选择变量">
|
||||||
|
{previousNodes.map((selector) => (
|
||||||
|
<Select.Option key={selector.id} value={selector.name}>
|
||||||
|
{selector.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 操作符 */}
|
||||||
|
<Form.Item {...restField} name={[name, "operator"]} className="mb-0 w-32" rules={[{ required: true, message: "请选择" }]}>
|
||||||
|
<Select>
|
||||||
|
<Select.Option value="==">等于 (==)</Select.Option>
|
||||||
|
<Select.Option value="!=">不等于 (!=)</Select.Option>
|
||||||
|
<Select.Option value=">">大于 (>)</Select.Option>
|
||||||
|
<Select.Option value=">=">大于等于 (>=)</Select.Option>
|
||||||
|
<Select.Option value="<">小于 (<)</Select.Option>
|
||||||
|
<Select.Option value="<=">小于等于 (<=)</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 右侧常量输入框 */}
|
||||||
|
<Form.Item {...restField} name={[name, "rightValue"]} className="mb-0 flex-1" rules={[{ required: true, message: "请输入值" }]}>
|
||||||
|
<Input placeholder="输入值" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 添加条件按钮 */}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() =>
|
||||||
|
add({
|
||||||
|
leftSelector: undefined as unknown as WorkflowNodeIOValueSelector,
|
||||||
|
operator: "==",
|
||||||
|
rightValue: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
block
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
添加条件
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
{formModel.conditions && formModel.conditions.length > 1 && (
|
||||||
|
<Form.Item name="logicalOperator" label="条件逻辑">
|
||||||
|
<Radio.Group buttonStyle="solid">
|
||||||
|
<Radio.Button value="and">满足所有条件 (AND)</Radio.Button>
|
||||||
|
<Radio.Button value="or">满足任一条件 (OR)</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default memo(ConditionNodeConfigForm);
|
||||||
|
|
@ -165,6 +165,10 @@ export type WorkflowNodeConfigForNotify = {
|
|||||||
providerConfig?: Record<string, unknown>;
|
providerConfig?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowNodeConfigForCondition = {
|
||||||
|
expression: Expr;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForBranch = never;
|
export type WorkflowNodeConfigForBranch = never;
|
||||||
|
|
||||||
export type WorkflowNodeConfigForEnd = never;
|
export type WorkflowNodeConfigForEnd = never;
|
||||||
@ -185,6 +189,32 @@ export type WorkflowNodeIOValueSelector = {
|
|||||||
|
|
||||||
// #endregion
|
// #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) => {
|
const isBranchLike = (node: WorkflowNode) => {
|
||||||
return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch;
|
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[] = [];
|
const outputs: WorkflowNode[] = [];
|
||||||
|
|
||||||
@ -445,10 +485,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type:
|
|||||||
return true;
|
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({
|
output.push({
|
||||||
...current,
|
...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;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export type WorkflowState = {
|
|||||||
addBranch: (branchId: string) => void;
|
addBranch: (branchId: string) => void;
|
||||||
removeBranch: (branchId: string, index: number) => void;
|
removeBranch: (branchId: string, index: number) => void;
|
||||||
|
|
||||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[];
|
getWorkflowOuptutBeforeId: (nodeId: string, type?: string) => WorkflowNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
@ -243,7 +243,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => {
|
getWorkflowOuptutBeforeId: (nodeId: string, type: string = "all") => {
|
||||||
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user