当前位置:   article > 正文

无代码平台工作流设计器,来看下吧_antv x6 工作流

antv x6 工作流

前沿

上一篇我们介绍了开源的无代码开发平台Brick, 在无代码开发中比较常见的一个功能是工作流设计器,工作流可以做流程审批设计,数据操作设计等。我们基于antv的x6
封装一个通用的能力,剩下的只需扩充节点类型就可以。

演示

brick演示地址

image.png

image.png

设计分析

我们看上面的演示功能,主要有三个核心模块。

  • 工作流画布(Workflow)
  • node节点设置 (NodeSetting)
  • node节点显示 (Node)

工作流画布

画布初始化

  1. 初始化工作流,为了方便统一管理,我们使用一个自定义hooks去管理画布的初始化.
export const useWorkflowInit = (container: React.RefObject<HTMLDivElement>) => {

  const currGraphRef = useRef<Graph>();
  const update = useUpdate();

  useEffect(() => {
    const graph = new Graph({
      container: container.current,
      autoResize: true, //自动变更画布大小
      grid: true,
      panning: false, //可以拖拽画布
      // mousewheel: true, //滚轮缩放画布
      background: {
      },
      connecting: {},
      onEdgeLabelRendered: (args: any) => {
        const { selectors, edge } = args;
        const content = selectors.foContent as HTMLDivElement;
        if (content) {
          ReactDOM.render(<EdgeLabel edge={edge} />, content);
        }
      },
    });
    setWorkflowElement(container.current);
  }, [container.current]);

  return {
    graph: currGraphRef.current,
  };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  1. 使用hooks做初始化
export const Workflow: FC<IWorkflowProps> = ({ style, className, data }) => {
  const { graph } = useWorkflowInit(containerRef);
  return (
    <div style={style} className={classNameStr}>
      <div ref={containerRef} className={s.container} id="container"></div>
    </div>
  );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

全局数据管理

  1. 我们使用context做数据管理,方便我们在组件内去使用相关信息和操作的相关功能等。

export class WorkflowAppProcessor {
  self: WorkflowAppProcessor;

  graphProcessor: GraphProcessor;

  workflowElement: HTMLElement | null;

  /**
   * node 节点相关内容
   */
  nodeModule: TNodeModuleMap;

  //工作留数据
  workflowData: Observable<IWorkflowEntity>;

  activeNode: Observable<IWorkflowNodeData | null>;

  constructor() {
    this.self = this;
    this.activeNode = observable(null);
    this.workflowData = observable({} as IWorkflowEntity);
    this.workflowElement = null;

    this.graphProcessor = createGraphProcessor().processor;
    this.nodeModule = getNodeModule();
  }

  setWorkflowElement = (element: HTMLDivElement) => {
    this.workflowElement = element;
  };

  /**
   * 设置workflow数据
   * @param data
   */
  setWorkflowData = (data: IWorkflowEntity) => {
    this.workflowData.set(data);
  };

  /**
   * 根据类型获取节点的默认数据
   * @param nodeType
   * @param defaultNodeData
   */
  _getDefaultNodeData = (nodeType: TNodeType, defaultNodeData?: Partial<IWorkflowNodeData>) => {
 
  };

  addNodeData = (nodeType: TNodeType, defaultNodeData?: Partial<IWorkflowNodeData>) => {
  };

  /**
   * 修改node data
   * @param nodeData
   */
  updateNodeData = (nodeData: IWorkflowNodeData) => {
     ...
  };
  ....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

node节点和setting

我们看到节点类型会有很多,我们可以按照约定去注册使用节点。

注册

  1. 我们用两个类型举例

看下面的截图,为每个类型分别创建了三个文件。

  • entry.ts 入口文件
  • Setting.tsx 节点设置器
  • AddData.tsx ... 节点显示组件

image.png

  1. entry.ts 入口文件功能

export class AddDataNode extends BaseNode {
  
  // 获取节点显示组件
  static getNodeElement = (): TLazyFunctionComponent => {
    return React.lazy(() => import('./AddData'));
  };

// 获取节点设计组件
  static getSettingPanel = (): TLazyFunctionComponent => {
    return React.lazy(() => import('./Setting'));
  };

  // 元数据信息
  static getMetadata = (): ISettingPanelMetaData => {
    return {
      name: '新增数据',
      type: ENodeType.AddData,
      icon: React.createElement(PlusOutlined),
    };
  };

 // 默认配置数据
  static getDefaultConfigData = () => {
    return {} as IAddDataNodeConfig;
  };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

看到 AddDataNode继承了BaseNode,我们可以在BaseNode去做实现约束。在入口文件中的getNodeElementgetSettingPanel方法中使用了React.lazy,可以帮我们实现异步加载这些组件。

  1. Setting.tsx 节点设置器

在Setting中封装一个SettingFormItem,是对antd的FormItem做了封装,提供设置的变更

const Setting: FC<ISettingComponentProps<ENodeType.AddData>> = (props) => {
  const { nodeData } = props;
  return (
    <div>
      <SettingFormItem
        title={'选择表单'}
        formItemProps={{
          name: ['tableId'],
        }}
      >
        <AppTableCaseCadeSelect />
      </SettingFormItem>
    </div>
  );
};

export default Setting;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 节点显示组件
    通过props传递nodeData,可以方便的获取节点的数据来做显示
const TableEvent: FC<INodeComponentProps<ENodeType.TableEvent>> = (props) => {
  const { nodeData } = props;

  const triggerEvent = nodeData?.config?.triggerEvent;
  ....

  return <div>{text}</div>;
};

export default TableEvent;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 获取节点信息,存储到全局对象中
import * as nodes from '../components/nodes';
export const getNodeModule = () => {
  const result: TNodeModuleMap = {} as TNodeModuleMap;

  Object.values(nodes).forEach((item) => {
    const metaData = item.getMetadata();
    const nodeType = metaData.type;

    const nodeModuleValue: INodeModuleValue = {
      nodeComponent: item.getNodeElement(),
      settingComponent: item.getSettingPanel(),
      metaData,
      defaultNodeConfigData: item.getDefaultConfigData?.() || {},
    };
    result[nodeType] = nodeModuleValue;
  });

  return result;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Setting和Node容器

  1. SettingContainer

主要从头全局状态中获取当前选中的node节点,根据节点的node类型去做对应的Setting展示。集成了antd的form组件,从而点确定的时候去更新全局需要存储的信息。


export interface ISettingContainerProps {}

export const SettingContainer: FC<ISettingContainerProps> = memo((props) => {
  const [activeNode, clearActiveNode, nodeModule, updateNodeData, nodeMap] = useWorkflowAppSelector(
    (s) => [s.activeNode, s.clearActiveNode, s.nodeModule, s.updateNodeData, s.workflowData.nodeMap]
  );

  const nodeId = activeNode?.id!;

  const [form] = Form.useForm();

  useEffect(() => {
    const values = nodeMap?.[nodeId] || {};
    form.setFieldsValue(values);
  }, [nodeId]);

  const onClose = useMemoizedFn(() => {
    clearActiveNode();
  });

  /**
   * 更新widget
   */
  const onOk = async () => {
    try {
      const values = await form.validateFields();

      updateNodeData({ ...values, id: nodeId });
      onClose();
    } catch (error: any) {
      const errMessage = error?.errorFields?.[0]?.errors?.[0];
      message.error(errMessage);

      return;
    }
  };

  const SettingComponent = nodeModule?.[activeNode?.type!]?.settingComponent;

  const Footer = () => {
    return (
      <div className={s.footer}>
        <Space>
          <Button onClick={onClose}>取消</Button>
          <Button type={'primary'} onClick={onOk}>
            确定
          </Button>
        </Space>
      </div>
    );
  };

  return (
    <Drawer
      title={activeNode?.name || '设置'}
      placement="right"
      bodyStyle={{
        padding: '24px 0',
      }}
      width={600}
      onClose={onClose}
      open={Boolean(activeNode)}
      footer={<Footer />}
    >
      <Form form={form}>
        <Suspense fallback={<div>Loading...</div>}>
          {SettingComponent && <SettingComponent nodeData={activeNode!} />}
        </Suspense>
      </Form>
    </Drawer>
  );
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  1. NodeContainer

主要也是通过当前激活的节点,去做对应的容器渲染。

export const NodeContainer = ({ node }: { node: Node }) => {

  const NodeComponent = nodeModule?.[nodeType]?.nodeComponent;

  return (
    <div
      onClick={onNodeClick}
    >
      {!isEnd && (
        <div className={s.content}>
          <div className={s.left}>{NodeComponent && <NodeComponent nodeData={currNode!} />}</div>
         
        </div>
      )}
    </div>
  );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

通过以上的设置,后续我们扩充节点只需要添加这三个文件就可以了。

画布操作

画布内的节点,连线等操作,我们封装一个通用的类来管理。


export class GraphProcessor extends BaseProcessor {
  // graph实例,不是
  graph: Graph | null;

  constructor() {
    super();
    this.graph = null;
    this.init();
  }

  private init = async () => {
    this.listeners();
  };

  /**
   * 设置graph实例
   * @param graph
   */
  setGraph = (graph: Graph) => {
    this.graph = graph;
    // @ts-ignore
    window._graph = graph;
  };

  /**
   * 添加node节点
   * @param nodeType
   * @param data
   */
  addNode = (nodeType: TNodeType, data: Node.Metadata) => {
    data.data = {
      ...data.data,
      type: nodeType,
    };
    if (!data.id) {
      data.id = uuid();
    }
    return this.graph?.addNode({ ...DEFAULT_NODE_ATTR, ...data });
  };

  addEdge = (source: string, target: string) => {
    return this.graph?.addEdge({
      source,
      target,
      attrs: {
        line: {
          stroke: '#8f8f8f',
          strokeWidth: 1,
        },
      },
      defaultLabel: {
        markup: Markup.getForeignObjectMarkup(),
        attrs: {
          fo: {
            width: 18,
            height: 18,
            x: -9,
            y: 0,
            // y: -(NODE_GAP / 2),
          },
        },
      },
      label: {
        // attrs: {
        //   text: {
        //     text: "s1"
        //   }
        // },
        position: {
          distance: -35,
          // distance: -(NODE_GAP / 2)
        },
      },
      router: {
        name: 'orth',
        args: {
          padding: {
            bottom: 10,
          },
        },
      },
    });
  };

  addNodeByEdge = ({
    nodeType,
    data,
    edge,
    isRedraw = true,
  }: {
    nodeType: TNodeType;
    data?: Node.Metadata;
    edge: Edge;
    isRedraw?: boolean;
  }) => {
    const sourceId = edge.getSourceCellId();
    const targetId = edge.getTargetCellId();

    this.addNode(nodeType, data!);
    // 当前线删除
    edge.remove();

    this.addEdge(sourceId, data!.id!);
    this.addEdge(data!.id!, targetId);

    if (isRedraw) {
      this.redraw();
    }
  };

  /**
   * 重新绘制实图
   */
  redraw = () => {
    const nodes = this.graph?.getNodes();
    const edges = this.graph?.getEdges();

    const graphArea = this.graph?.getGraphArea();
    // 画布宽度
    const graphWidth = graphArea?.width || 0;
    // 画布高度
    const graphHeight = graphArea?.height || 0;

    const connections =
      edges?.map((f) => ({
        sourceId: f.getSourceCellId(),
        targetId: f.getTargetCellId(),
      })) || [];

    // 1. 通过edges找出层级关系
    const treeLevelData = convertToLevelTree(connections);

    treeLevelData.forEach((currLevelData, level) => {
      // 当前级别数量
      const currLevenLength = currLevelData.length;

      // 当前级别node总宽度
      const nodeSumWidth = NODE_WIDTH * currLevenLength + NODE_GAP * (currLevenLength - 1);

      const currBeginX = (graphWidth - nodeSumWidth) / 2;

      currLevelData.forEach((nodeId, index) => {
        const currNode = this.graph?.getCellById(nodeId) as Node;

        if (currNode) {
          // const { width, height } = currNode.getSize();;

          currNode?.setPosition({
            x: currBeginX + (NODE_WIDTH + NODE_GAP) * index,
            y: NODE_GAP + (NODE_HEIGHT + NODE_GAP) * level,
          });
        }
      });
    });

    // 2. 根据层级关系重新渲染
  };

  /**
   * 删除node节点
   * @param node
   */
  removeNode = (node: Node) => {
    this.graph?.removeNode(node);
  };

  /**
   * 获取workflow数据
   */
  getData = () => {
    return this.graph?.toJSON();
  };

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176

其中的一些细节就不做详细介绍了,有需要的可以直接看代码。

联系我

建立了一个微信交流群,请添加微信号brickmaster1,备注brick,我会拉你进群

总结

整个工作流的代码在 b-workflow,有需要的,可以直接去查看

大家觉得有帮助,请在github帮忙star一下。

如果你觉得该文章不错,不妨

1、点赞,让更多的人也能看到这篇内容

2、关注我,让我们成为长期关系

3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/864538
推荐阅读
相关标签
  

闽ICP备14008679号