「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

前言:这是一篇面向没触摸过流程图完成的小白集体的文章,所以不会有较难明的原理的东西,看完你就会运用一套图流程引擎结构来完成一些流程图需求,

晋级流程图是什么?

在刚刚曩昔的英豪联盟S12中,或许我们还沉浸在中国队痛失决赛的悲痛中,洗把脸,日子仍是要过,跟着我,来学习一点新技能~

在各个直播渠道中,咱们肯定见过这样的部队晋级图:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

来自维基百科的图

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

来自b站直播页的图

现在翻开b站赛事专题页,咱们仍能够看到这样的流程图:

链接

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

假如咱们来自己完成,看看怎么用前端技能,来完成这样的 “流程图”。

技能调研和选型

剖析

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?
F12翻开b站页面,咱们发现阿b的程序猿是用div完成的节点加svg写死path完成的连线,这种完成有点迟钝,一旦产品和规划姐姐要求调整下方位/款式,svg的线path就得从头调数值,节点div方位款式也得从头调,不行灵敏,多加一些无用班。

选型和比照

所以我想用市道上的一些图引擎来完成,节点和线能够拖动调整,一同款式支撑自界说调整等。调研了下现在市道主流的结构:bpmn.js,x6,logicflow。

bpmn是国外搞得一套流程图结构,研制比较早,现已较完善,缺陷是文档只支撑英文,自界说扩展比较难,底层规划个人不是很喜欢,和现代前端结构思想有偏移,或许是做得比较早的原因。

x6和logicflow都是国内自研的图编辑引擎,归纳比较了下,决定选用logicflow来完成,首要是logicflow图开发根据MVVM,和咱们平时写vue react有类似之处,且支撑bpmn标准,底层用ts完成,提示友好,主打自界说节点和插件扩展上,完成起来更灵敏,不用忧虑发现做了一半完成不了的问题。

logicflow介绍

logicflow文档:logic-flow.org/

github地址:github.com/didi/LogicF… (点点star,鼓励开发者持续维护升级)

LogicFlow 是一款开源的流程图编辑结构,供给了一系列流程图交互、编辑所必需的功用和灵敏的节点自界说、插件等拓展机制。LogicFlow支撑前端研制自界说开发各种逻辑编列场景,如流程图、ER图、BPMN流程等。在工作审批装备、机器人逻辑编列、无代码渠道流程装备都有较好的应用。

为了便利,以下logicflow简称lf。

完成思路

大约阅读过一遍logicflow的文档后,对完成b站这样一个流程图心里有了一个大约思路:

  1. 动态创立几个方形节点。
  2. 节点自界说编码,做成b站流程图节点的样子。
  3. 拖拽这些节点到适宜的方位,调用logicflow的大局获取数据办法,保存到代码里。
  4. 拖拽节点之间连线,自界说连线款式,调用logicflow的大局获取数据办法,保存到代码里。
  5. 微调节点和连线的数据坐标,确保肯定对齐和一些重合作用等。
  6. ….(其他扩展功用)

编码

初始化

logiclfow与结构无关(vue,react都可运用),咱们用vue-cli创立一个工程,先new一个画布出来,这块能够参阅 参阅

想动态化创立快速创立几个节点出来,咱们能够直接启用logicflow内置的拖拽面板插件:

import LogicFlow from '@logicflow/core';
import "@logicflow/core/dist/style/index.css";
import { DndPanel, SelectionSelect } from '@logicflow/extension';
import '@logicflow/extension/lib/style/index.css'
const lf = new LogicFlow({
  container: document.querySelector('#graph'),
  plugins: [DndPanel, SelectionSelect]
});
lf.extension.dndPanel.setPatternItems([
  {
        type: 'rect',
        label: '矩形节点',
        icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
        className: 'important-node'
      }
]);

观察b站的流程图,咱们发现需求拖进画图14个矩形节点,大约拖进来后长这样子,接下来咱们经过调用实例上的办法lf.getGraphData()即可获取到当时画布的数据保存下来,以免改写丢失。

能够在画布外设置一个按钮,调用获取图数据,做实时保存。保存下来的数据大约长这样:

export const groupData = {
  nodes: [
    { id: '87692456-fc48-4b0e-b7d8-b5bb79822ae5', type: 'rect', x: 308, y: 74, properties: {} },
    { id: 'f3650805-cb9e-4e0e-aa74-1ae1de198bb3', type: 'rect', x: 304, y: 148, properties: {} },
    { id: 'a6e4643d-6131-43e2-9c7f-480c531192b7', type: 'rect', x: 312, y: 264, properties: {} },
    { id: '2a58edac-442c-47d2-a3bd-77767c17b26f', type: 'rect', x: 316, y: 340, properties: {} },
    { id: 'ce97cc95-882f-449e-83d5-e5e0141eadd4', type: 'rect', x: 304, y: 456, properties: {} },
    { id: 'd4bb5170-be9d-4000-a8bf-2b2b6d0804a6', type: 'rect', x: 308, y: 535, properties: {} },
    { id: '834a95a3-3637-44b8-b5c9-7fc8d7e50c87', type: 'rect', x: 313, y: 634, properties: {} },
    { id: 'd68757e6-24d4-4ab4-94cd-ab131f9b788a', type: 'rect', x: 304, y: 710, properties: {} },
    { id: 'cc1cde7f-daad-4d04-9cad-8ed8d406a8be', type: 'rect', x: 557, y: 263, properties: {} },
    { id: '9934569e-cdb2-48b9-9b3a-c4de39832b27', type: 'rect', x: 556, y: 336, properties: {} },
    { id: 'e7e87057-6e31-4945-aec2-a547308e1de8', type: 'rect', x: 555, y: 457, properties: {} },
    { id: '089384e0-a0ab-489a-aa26-c7c4e8b4610f', type: 'rect', x: 559, y: 536, properties: {} },
    { id: 'be77ef9e-0b9d-41a8-86d7-b29b41160f77', type: 'rect', x: 817, y: 358, properties: {} },
    { id: '393b8aee-11a0-4007-a38f-5f0dde2e2526', type: 'rect', x: 821, y: 442, properties: {} }
  ],
  edges: []
};

这种数据格式是lf底层烘托标准,各个参数大约意思是:

  • id,每个节点/连线独一无二的身份id
  • type, lf内置的的节点/边类型
  • x,y,遵从svg的坐标标准,为图形左上角的坐标。留意和下文中从model去取的x,y不同,model的x,y是图形中心的坐标

然后用 lf.render(groupData)烘托这部分数据,这样咱们基本结构就算完成了。

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

自界说节点

由于b站的流程图节点长这样子,现在的款式和功用还没到达咱们要求,接下来咱们先“包装加工”下咱们的节点

logicflow经过lf.register注册的办法来注册自己的节点,咱们创立一个自界说节点文件(叫TBD-node),依据持续看logicflow规矩,一个节点需求包括Model类和View类,这两个类的作用:

  • Model类 在model中经过getNodeStyle()钩子函数,从头节点款式,这个函数在节点数据properties改动时会触发履行。

  • View类 在view中,咱们能够在钩子函数getShape中,经过调用lg供给的h函数(类似vue的createlement函数),来自界说烘托咱们的节点svg dom。

这里需求留意lf的底层规划:

虽然自界说节点view优先级最高,功用也最完善,理论上咱们能够完全经过自界说节点view完成任何咱们想要的作用,但是此办法仍是存在一些约束。

  1. 自界说节点view终究生成的图形的形状特点有必要和model中形状特点的共同,因为节点的锚点、外边框都是根据节点model中的width和height生成。

  2. 自界说节点view终究生成的图形全体概括有必要和承继的根底图形共同,不能承继的rect而在getShape的时候返回的终究图形概括变成了圆形。因为LogicFlow关于节点上的连线调整、锚点生成等会根据根底图形进行核算。

重写节点款式

咱们在第一步拿到的画布数据根底上,给节点数据的properties特点增加’win’和’lose’字段,用来区分b站流程图中两种节点的款式。并经过重写getNodeStyle函数自界说节点款式:

  /**
   * 重写节点款式
   */
  getNodeStyle() {
    const style = super.getNodeStyle();
    const { result } = this.properties;
    if (result === 'win') {
      style.fill = "#0094ff"
    } else {
      style.fill = "#f1f2f3"
    }
    style.stroke = '#EAEAEC'
    style.strokeWidth = 1;
    return style;
  }

fill, stroke都是svg的根底特点,能够参阅MDN

重写节点图形

观察b站流程图节点款式,咱们发现除了首要的矩形节点,还有右边一个暗影矩形,左面一个图标,一个部队文本,一个比分文本,经过自界说绘制image,rect,text 三种svg即可:

这里首要留意的rect中的x,y是svg标准标准的左上角,model中的x,y是图形中心的坐标,从model层读取坐标数据后在view层要核算一下

  getShape() {
    const {
      text,
      x,
      y,
      width,
      height,
      radius
    } = this.props.model;
  // 省掉一些款式判别代码
    return h(
      'g',
      {
        className: 'lf-TBD-node',
      },
      [
        h('rect', {
          ...style,
          x: x - width / 2,
          y: y - height / 2,
          width,
          height,
          rx: radius,
          ry: radius
        }),
        h('g', {
          style: 'pointer-events: none;',
          transform: `translate(${x}, ${y})`
        }, [
          h('rect', {
            x: width/2-26 ,
            y:  -22,
            width: 26,
            height: 44,
            fill: '#000',
            stroke: 'none',
            ...scoreBack
          }),
          h('text', {
            x: width/2-18 ,
            y: 5,
            style: scoreTextStyle
          }, [score]),
          h('text', {
            x: -80 ,
            y: 5,
            style: teamNameTextStyle
          },[name]),
          h('image', {
            width: 35,
            height: 35,
            x: - width / 2 + 3,
            y: - height / 2 + 3,
            href: getIcon(name)
          })
        ])
      ]
    )
  }

在lg中的view类中, model的所有特点都经过props办法传入,取值同理,经过自界说特点properties来判别给win的部队节点和lose的部队节点设置不同款式。

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

代码:

  // 上面代码片段中省掉的款式代码
  const style = this.props.model.getNodeStyle()
    const score = this.props.model.properties.score || 0;
    const { result, name } = this.props.model.properties;
    let scoreTextStyle = '', teamNameTextStyle = '', scoreBack = {};
    if (result === 'win') {
      teamNameTextStyle = "fill:#fff;";
      scoreTextStyle = "fill: #fff;";
      scoreBack = {
        fill : 'rgb(66,49,49)',
        fillOpacity: 0.3,
      }
    } else {
      teamNameTextStyle = 'fill: #9499a0;';
      scoreTextStyle = 'fill: #9499a0;';
      scoreBack = {
        fillOpacity: 0.1,
      }
    }
    teamNameTextStyle += "font-size: 14px;font-family: Helvetica, Arial, sans-serif;text-overflow: ellipsis;letter-spacing: 0;"
    scoreTextStyle += "font-size: 18px;font-family: Helvetica, Arial, sans-serif;"

自界说线

和自界说节点相似,lg也能够自界说线的款式和功用,首要咱们先承继lg内置的Polyline折线类型。为了模仿b站流程图中线的样子,咱们要在Model层的getEdgeStyle()办法中重写线的款式,在view类中的getArrow()办法中将线的箭头视图躲藏掉。

代码:

import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core';
class BetterLineModel extends PolylineEdgeModel {
  getEdgeStyle() {
    const style = super.getEdgeStyle();
    style.stroke = '#9499a0';
    style.strokeWidth = 1;
    style.strokeLinecap = 'butt';
    style.strokeLinejoin = 'miter';
    style.fill = 'transparent';
    return style;
  }
}
class BetterLine extends PolylineEdge {
  getArrow() {
    return null;
  }
}
export default {
  type: 'better-line',
  view: BetterLine,
  model: BetterLineModel
};

封装插件

lf 供给了插件机制来完成封装和复用,比方上文中咱们完成的b站风格的流程图部队节点和连线,假如其他页面的流程图也想复用,就需求咱们把这个全体封装成一个lf的plugin。

lf的plugin标准也很简单,声明一个plugin类,在这个类中注册你的节点和线,默许导出即可:

封装插件:

import TBDNode from './TBD-node';
import betterLine from './better-line';
import type { LogicFlow } from '@logicflow/core';
class S12Plugin {
  static pluginName = 's12';
  lf: LogicFlow;
  constructor({ lf }) {
    this.lf = lf;
    this.lf.register(TBDNode);
    this.lf.register(betterLine);
  }
}
export {
  S12Plugin,
};

运用插件:

    const lf = new LogicFlow({
      container: this.$refs.container,
      width: 1300,
      height: 700,
      plugins: [ S12Plugin]
    });

微调

比对现在作用,咱们还需求微调一些款式。 一个是躲藏锚点,lf每个节点默许有4个锚点(出线的当地),分别在矩形节点的四个边中心,咱们点击锚点能够拉出logicflow的连线出来,链接到目标节点的锚点上,连接完线后咱们不需求再显示锚点。

重写锚点款式在model层重写getAnchorStyle()办法:

  getAnchorStyle (anchorInfo) {
    const style = super.getAnchorStyle(anchorInfo);
    style.fill = 'transparent';
    style.stroke = 'transparent';
    style.hover.fill = 'transparent';
    style.hover.stroke = 'transparent';
    return style;
  }

第二个是微调数据,拖拽究竟不能确保两条线完全重合,比方:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?
在数据中找到其中一条线,如下图,pointList便是折线的从起点开端包括拐角点到终点的所有点坐标调集,微调其中的y坐标数据即可。
「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

微调后,锚点和线都完整重合啦:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

至此,咱们现已完成了仿b站赛事专题页英豪联盟专题的一个部队晋级流程图了:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

完整代码:github

预览地址: 预览

持续思考 & 加需求

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

问题:假如咱们在做的部队晋级流程图需求面向的是正在进行的比赛,如下图,许多晋级部队方位仍是TBD状况(如上图)。产品小姐姐要求咱们比赛成果一出来,就能立刻更新流程图新的状况。

lf的UI是靠画图数据驱动,咱们只要能快速生成对应的节点线数据,就能够快速更新流程图的UI。

在本文上面最开端的初始化章节中,咱们便是经过拖拽生成节点,然后调用大局保存图数据的办法,初始化了根底结构。同理,针对上诉需求,咱们能够在后台做一个拖拽装备功用,如:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

这这个图中,咱们按照上面的自界说节点办法,又完成了左边的team-node节点,咱们的需求是希望拖动左边部队节点到右侧流程图TBD节点时,能把TBD节点换成新的部队节点,方位不变。

这样咱们再调用获取图数据按钮,就能够拿到新的数据,再发布更新到线上,就能够满足产品小姐姐快速更新流程图成果的需求啦。

规划完成

检查lf文档可知链接,lf供给了丰厚的各种当时流程图产生的事情,咱们需求的事情有:

  • node:dnd-drag 外部拖入节点添加时触发
  • node:dnd-add 外部拖入节点添加时触发

思路规划:当拖拽左面team-node时,利用dnd-drag不断判别team-node节点是否接近TBD节点,接近时,TBD节点高亮,表示已到能够更新TBD节点的状况,一同触发node:dnd-add事情,咱们在该事情中删除team-node节点,一同拿到team-node节点的数据(部队名,图标等),更新TBD节点即可

大约完成:

import TeamNode from './team-node';
import betterLine from './better-line';
import type { LogicFlow } from '@logicflow/core';
import debounce from 'lodash.debounce';
class S12Plugin {
  static pluginName = 's12';
  lf: LogicFlow;
  constructor({ lf }) {
    this.lf = lf;
    this.lf.register(TeamNode);
    this.lf.register(betterLine);
    this.lf.on('node:dnd-drag', debounce(this.checkAppendBoundaryEvent, 10));
    this.lf.on('node:dnd-add', this.appendBoundaryEvent);
  }
  // 假如拖拽到节点内,更新TBD节点数据,一同删除拖动的team-node节点
  private appendBoundaryEvent = ({ data }) => {
    console.log('node:dnd-add')
    const closeNodeId = this.checkAppendBoundaryEvent({ data })
    if (closeNodeId) {
      const nodeModel = this.lf.graphModel.getNodeModelById(closeNodeId)
      nodeModel.setIsCloseToBoundary(false)
      nodeModel.text.value = data.text.value
      nodeModel.setProperties(data.properties)
    }
    this.lf.deleteNode(data.id)
  }
  // 检测拖拽节点时,有没有拖拽到TBD节点内
  private checkAppendBoundaryEvent = ({ data }) => {
    const { x, y, id } = data;
    const { nodes } = this.lf.graphModel;
    let closeNodeId = '';
    for (let i = 0; i < nodes.length; i++) {
      const nodeModel = nodes[i];
      if (nodeModel.id !== id && nodeModel.isTeamNode) {
        if (this.isCloseNodeEdge(nodeModel, x, y) && !closeNodeId) { // 一同只允许在一个节点的鸿沟上
          nodeModel.setIsCloseToBoundary(true);
          closeNodeId = nodeModel.id;
        } else {
          nodeModel.setIsCloseToBoundary(false);
        }
      }
    }
    return closeNodeId;
  }
  // 判别是否这两个节点接近
  private isCloseNodeEdge (nodeModel, x, y) {
    if (Math.abs(nodeModel.x - x) < 30 && Math.abs(nodeModel.y - y) < 10) {
      return true
    }
    return false
  }
}
export {
  S12Plugin,
  TeamNode,
};
  //team-node.ts
  /**
   * 供给办法给插件在判别此节点被拖动鸿沟事情节点接近时调用,从而触发高亮
   */
  setIsCloseToBoundary (flag) {
    // 每次setProperty更改Property特点时,lf内部会从头re-render
    this.setProperty('isCloseToBoundary', flag)
  }

作用:

「bilibili赛事专题页」中的英豪联盟「晋级流程图」是怎么完成的?

这个功用代码和上面b站不在一版,代码地址链接

我们有什么问题,欢迎下方留言一同探讨。

附录logicflow github地址