低代码可视化逻辑编排——低代码知识点详解(五)

4,950 阅读5分钟

demo体验地址:dbfu.github.io/lowcode-dem…

前言

前面我们实现了组件事件绑定动作,但是一个事件只能绑定一个动作,大大限制了开发复杂功能的能力。

这一篇来实现一个事件可以绑定多个动作,并且通过可视化的方式设置,让流程配置清晰明了。

思路来源于虚幻游戏引擎的蓝图功能,这一篇先简单实现一下,主要是给大家分享一下实现思路。

可视化库使用antvG6开源库,功能强大,使用起来也简单。

往期回顾

《保姆级》低代码知识点详解(一)

低代码事件绑定和组件联动——低代码知识点详解(二)

低代码动态属性和在线执行脚本——低代码知识点详解(三)

低代码在线加载远程组件——低代码知识点详解(四)

案例分析

上一篇文章后,留了一张图,下面给大家分析一下这张图的意义。

image.png

触发某个事件后,执行组件方法,然后动作执行成功后调一个接口,接口执行成功或失败后执行动作。

从上图可以得知:

每个组件的每个事件都可以绑定一个事件流,当触发组件事件的时候,执行这个事件流。

事件流设计页面,有4种类型节点,第一个是开始节点,第二个是动作节点、第三个是条件节点、第四个是事件节点,开始节点也算是事件节点。

开始节点:没有意义,表示入口。

动作节点:可以绑定动作,可以连接事件节点和条件节点

条件节点:可以配置多个条件分支,每个条件分支可以绑定一个动作节点,只能连接事件节点。

事件节点:每个动作节点都会有事件节点,只能连接动作节点。

效果展示

上图可以使用antv的G6库来实现,因为官方给的demo中有类似的案例,可以参考使用。

image.png

经过一番改造,终于实现了需求。

image.png

动作节点配置

image.png

image.png

条件节点配置

image.png

image.png

image.png

image.png

核心功能讲解

前言

这一块内容比较多,我挑几个核心功能和大家分享一下。

数据结构

节点数据结构

export interface Node {
  /**
   * 节点id
   */
  id: string;
  /**
   * 节点描述
   */
  label: string;
  /**
   * 节点类型
   */
  type: string;
  /**
   * 节点绑定的下拉菜单
   */
  menus: Menu[];
  /**
   * 节点子节点
   */
  children?: Node[];
  /**
   * 节点额外配置
   */
  config?: any;
  /**
   * 节点条件结果
   */
  conditionResult?: boolean;
  /**
   * 节点事件key
   */
  eventKey?: string;
}

下拉菜单

export interface Menu {
  /**
   * 菜单key
   */
  key: string;
  /**
   * 菜单描述
   */
  label: string;
  /**
   * 即将生成的节点类型
   */
  nodeType?: string;
  /**
   * 将生成的节点名称
   */
  nodeName?: string;
  /**
   * 如果节点为条件节点,每个菜单表示一个条件,conditionId对应定义的条件id
   */
  conditionId?: string;
}

初始数据

export const data: Node = {
  id: 'root',
  label: '开始',
  type: 'start',
  menus: [
    {
      key: 'action',
      label: '动作',
      nodeType: 'action',
      nodeName: '动作',
    },
    {
      key: 'condition',
      label: '条件',
      nodeType: 'condition',
      nodeName: '条件',
    },
  ],
};

实现画布居中偏上

G6提供了垂直水平居中方法 graph.fitCenter(),但是只支持垂直水平居中,根据上图我们只需要水平居中,所以在画布渲染后,需要用translate方法向上移动一下位置。

image.png

自定义带添加图标节点

import { COLLAPSE_ICON, EXPAND_ICON } from '../icons';

export const actionNode: any = {
  options: {
    style: {
      fill: '#F9F0FF',
      stroke: '#B37FEB',
      radius: 8,
      lineWidth: 1,
    },
    stateStyles: {
      hover: {},
      selected: {},
    },
    labelCfg: {
      style: {
        fill: '#000000',
        fontSize: 14,
        fontWeight: 400,
        fillOpacity: '0.7',
      },
    },
    size: [120, 40],
  },
  afterDraw(cfg: any, group: any) {
    const styles = this.getShapeStyle(cfg);
    const h = styles.height;
    const w = styles.width;

    const keyShape: any = group.addShape('rect', {
      attrs: {
        x: 0,
        y: 0,
        ...styles,
      },
    });

    group.addShape('marker', {
      attrs: {
        x: w / 2 - 20,
        y: 0,
        r: 6,
        stroke: '#ff4d4f',
        cursor: 'pointer',
        symbol: COLLAPSE_ICON,
      },
      name: 'remove-item',
    });

    if (cfg.menus?.length) {
      group.addShape('marker', {
        attrs: {
          x: 0,
          y: h / 2 + 7,
          r: 6,
          stroke: '#73d13d',
          cursor: 'pointer',
          symbol: EXPAND_ICON,
        },
        name: 'add-item',
      });
    }

    if (cfg.label) {
      group.addShape('text', {
        // attrs: style
        attrs: {
          x: 0, // 居中
          y: 0,
          textAlign: 'center',
          textBaseline: 'middle',
          text: cfg.label,
          fill: '#000000',
          fontSize: 12,
          fontWeight: 400,
          fillOpacity: '0.7',
        },
        name: 'text-shape',
      });
    }

    return keyShape;
  },
  update(cfg: any, node: any) {
    const styles = this.getShapeStyle(cfg);
    const h = styles.height;
    const group = node.getContainer();

    const child = group.find((item: any) => {
      return item.get('name') === 'add-item';
    });

    const text = group.find((item: any) => {
      return item.get('name') === 'text-shape';
    });

    if (text) {
      text.attr({ text: cfg.label })
    }

    if (!child && cfg.menus?.length) {
      group.addShape('marker', {
        attrs: {
          x: 0,
          y: h / 2 + 7,
          r: 6,
          stroke: '#73d13d',
          cursor: 'pointer',
          symbol: EXPAND_ICON,
        },
        name: 'add-item',
      });
    }
  },
};

根据是否有菜单动态添加加号图标

image.png

这里复杂一点的是算坐标,y为0的表示节点中心,所以需要下移半个节点高度(h/2),再加上图标的高度(+7),多加1是为了好看点。

连接线上加文本

image.png

如图所示,如果线的源节点是条件节点,获取条件名称,添加到线上。

image.png

实现下拉菜单

监听添加按钮点击事件,获取当前节点位置,根据当前节点的位置和菜单配置渲染下拉菜单。

// src/editor/layouts/flow-event/context-menu.tsx

import React from 'react';
import { Dropdown } from 'antd';

interface Props {
  position: {
    top?: number;
    left?: number;
  };
  onSelect: (item: any) => void;
  items: { label: string, key: string }[];
  open: boolean;
}

const ContextMenu: React.FC<Props> = (props) => {
  const { position, onSelect, items, open } = props;

  return (
    <div
      style={{
        position: 'absolute',
        top: position?.top,
        left: position?.left,
      }}
    >
      <Dropdown
        menu={{
          items: items.map(item => ({ label: item.label, key: item.key })),
          onClick: onSelect
        }}
        open={open}
      >
        <a onClick={(e) => e.preventDefault()}></a>
      </Dropdown>
    </div>
  );
}

export default ContextMenu;

image.png

执行事件流

遍历节点,判断是否是动作节点,如果是动作节点并且条件结果不为false,则根据不同动作类型执行不同动作;如果是条件节点,执行条件脚本,把结果注入到子节点conditionResult属性中。

image.png

显示提示动作实现

image.png

组件方法动作实现

image.png

设置变量动作实现

image.png

执行脚本动作实现

image.png

最后

这一篇我们实现了事件可视化配置,下一篇会封装一些常用组件,并且使用低代码做一个完整功能实战一下。

demo体验地址:dbfu.github.io/lowcode-dem…

demo仓库地址:github.com/dbfu/lowcod…