阅读 120

供应链仿真系统之前端组件篇

背景

越来越多的仿真项目进行的如火如荼之际,相比传统的表单方式的仿真,零售通前端一直在思考,是否有更便捷、更人性化、更高效率的仿真交互。对于用户来说,最人性化的交互莫过于可视化的所见既所得,通过简单的拖拽编排来模拟和仿真零售通各业务参数,实时得到结果反馈。

在开始确定仿真系统的交互选型时,我们调研了业界和阿里内部的许多图形交互方案,发现图形交互场景大多数是两种,一种是流程编排,基于内置的一些图形元素来排版一些简单的业务流程,比如:bpmn、xflow等;一种是图表制作,也是基于一些内置的图表组件来搭建一个报表页面,比如:d3、fbi等。

但是它们有一个共有的缺陷:封闭,图形定制能力封闭,模型绑定能力封闭,输出结果封闭。基于这些方案,想要定制图形元素、定制流程,定制输出会非常麻烦,甚至是不可能。而我们的场景是供应链仿真,不同于传统意义上的流程编排,更不是报表页面的生成,我们需要足够灵活的图形定制能力,松散的模型绑定和自由化的输出能力。

经过深思熟虑,最终我们选择了业界著名的图形交互库gojs,它有强大的图形定制能力,有丰富的文档和足够多的帮助实例,上手容易而且表现力非常突出。在业务落地过程中,我们沉淀了一套图形交互组件,并接入了表单领域的schema能力,既可以画布元素之间直接交互,也可以通过表单的能力辅助交互。

我们希望该图形交互方案可以填补部分公司内图形交互领域的空白。

正文

本篇文章不介绍艰涩的架构方案,而是专注介绍图形UI组件以及通过这些组件我们可以做到什么。

本文主要介绍四个UI组件:拖拽池组件、画布组件、节点元素组件、线条元素组件,另附三个业务组件:仓库组件、区域组件、关系组件。

大家都知道,一个所见即所得的可视化界面,离不开三个要素,拖拽池、画布、配置面板。

下面重点介绍拖拽池组件和画布组件:

拖拽池组件

该组件可看成是另一种画布,和画布一样,里面内置了大量的节点模版以供选择,也提供了自定义模版的钩子。注意,与传统的拖拽交互不同,从这里拖拽出来的不是具体的节点元素,而是节点所包含的数据,其在画布上如何展示取决于对应的模版。

组件级配置

元素级配置

<DiagramDragPanel
   dataSource={[
     { "key": "collection", "text": "集货仓", "src": "https://img.alicdn.com/tfs/TB1BflsxpzqK1RjSZFoXXbfcXXa-90-60.svg" },
     { "key": "sale", "text": "销售仓", "src": "https://img.alicdn.com/tfs/TB1WvlQxxjaK1RjSZKzXXXVwXXa-91-61.svg" },
     { "key": "allocation", "text": "分拨中心", "src": "https://img.alicdn.com/tfs/TB1WvRsxwHqK1RjSZFPXXcwapXa-90-60.svg" },
     { "key": "cloud", "text": "云仓", "src": "https://img.alicdn.com/tfs/TB117kYxmzqK1RjSZPcXXbTepXa-90-61.svg" }
   ]}
/>
复制代码

面板中的元素,通过简单的图片就可以生成,除了src是展示必填的,key和text都可以选填,key如果不填,组件会随机生成唯一的key值。

画布组件

画布中内置了大量的节点模版和线条模版(详细介绍在下面),并提供了自定义模版的钩子,通过拖拽或直接声明的方式可以渲染成任何你想要的展现形式。

组件级配置

元素级配置

<DiagramCanvasPanel
   dataSource={{
      "nodeDataArray": [
        {"key":"w2", "text":"仓库二", "src":"https://img.alicdn.com/tfs/TB117kYxmzqK1RjSZPcXXbTepXa-90-61.svg", "location":"-212.00000000000006 -67.00000000000001"},
        {"key":"w3", "text":"仓库三", "src":"https://img.alicdn.com/tfs/TB1WvRsxwHqK1RjSZFPXXcwapXa-90-60.svg", "location":"-45 -67"},
        {"key":"w1", "text":"仓库一", "src":"https://img.alicdn.com/tfs/TB1WvlQxxjaK1RjSZKzXXXVwXXa-91-61.svg", "location":"-36.000000000000014 -347.9999999999999"},
        {"key":"110000", "text":"北京", "src":"https://img.alicdn.com/tfs/TB1mOJqxpYqK1RjSZLeXXbXppXa-90-85.svg", "location":"150 -266"},
        {"key":"120102", "text":"河东区", "src":"https://img.alicdn.com/tfs/TB19lxixzDpK1RjSZFrXXa78VXa-90-84.svg", "location":"185 -148"},
        {"key":"140200", "text":"大同市", "src":"https://img.alicdn.com/tfs/TB1S9prxpzqK1RjSZSgXXcpAVXa-90-84.svg", "location":"105, -67"},
        {"key":"140203004", "text":"永定庄街道", "src":"https://img.alicdn.com/tfs/TB1PVRsxAvoK1RjSZFNXXcxMVXa-91-84.svg", "location":"105, 33"},
        {"key":"150000", "text":"内蒙古自治区", "src":"https://img.alicdn.com/tfs/TB1mOJqxpYqK1RjSZLeXXbXppXa-90-85.svg", "location":"68.9999999999999 180.00000000000003"},
        {"key":"310101", "text":"黄浦区", "src":"https://img.alicdn.com/tfs/TB19lxixzDpK1RjSZFrXXa78VXa-90-84.svg", "location":"-397 -203.005078125"},
        {"key":"310104", "text":"徐汇区", "src":"https://img.alicdn.com/tfs/TB19lxixzDpK1RjSZFrXXa78VXa-90-84.svg", "location":"-397 -105"},
        {"key":"310105", "text":"长宁区", "src":"https://img.alicdn.com/tfs/TB19lxixzDpK1RjSZFrXXa78VXa-90-84.svg", "location":"-397 -4.005078124999997"},
        {"key":"310106", "text":"静安区", "src":"https://img.alicdn.com/tfs/TB19lxixzDpK1RjSZFrXXa78VXa-90-84.svg", "location":"-397 89.994921875"}
      ],
      "linkDataArray": [
        {"from":"w2", "to":"w3", "category": "flow", "points":[ -161.5,-52,-151.5,-52,-100.5,-52,-100.5,-52,-49.5,-52,-39.5,-52 ]},
        {"from":"w3", "to":"w1", "points":[ -17,-67,-17,-77,-17,-77,-8,-77,-8,-308,-8,-318 ], "stroke":"#f11cf2"},
        {"from":"w1", "to":"w3", "category":"dotted", "points":[ -15.736197508356163,-318,-56.21348980611415,-239.93601904160474,-59.944375775149695,-160.81240751504214,-22.955671867136942,-67 ]},
        {"from":"w1", "to":"w2", "category":"DoubleDash", "points":[ -29.664737265476692,-317.9999999999999,-112.17281942587421,-260.8739330073415,-164.57614089681135,-177.20726634067492,-181.67299538546686,-67 ]},
        {"from":"w3", "to":"110000", "uponLinkElements": [{"src": "https://img.alicdn.com/tfs/TB1dSFFxxjaK1RjSZFAXXbdLFXa-90-85.svg","text": "航空配送商","segmentOffset":"0 43"}, {"src": "https://img.alicdn.com/tfs/TB1mCFFxxjaK1RjSZFAXXbdLFXa-90-85.svg","text": "直送配送商","segmentOffset":"0 -39"}], "points":[ 5.5,-52,15.5,-52,12,-52,12,-52,20,-52,20,-251,140,-251,150,-251 ]},
        {"from":"w3", "to":"120102", "points":[ 5.5,-57,15.5,-57,12,-57,12,-57,20,-57,20,-133,180.4999999999998,-133,190.4999999999998,-133 ]},
        {"from":"w3", "to":"140200", "points":[ 5.5,-52,15.5,-52,15.5,-52,15.5,-52,100.5,-52,110.5,-52 ]},
        {"from":"w3", "to":"140203004", "points":[ -9.5,-37,-9.5,-27,-9.5,-28,-9.5,-28,-9.5,48.00000000000001,112.49999999999996,48.00000000000001,122.49999999999996,48.00000000000001 ], "stroke":"#1600ff"},
        {"from":"w3", "to":"150000", "points":[ -24.5,-37,-24.5,-19,-24.5,-20,-24.5,-20,-24.5,108,114.99999999999994,108,114.99999999999994,170,114.99999999999994,180 ], "stroke":"#fe2501"},
        {"from":"110000", "to":"w2", "category":"dotted", "points":[ 150,-246.98963755203582,21.796187194219268,-224.1388039270521,-82.03714613911406,-166.1785421224705,-170.60481400898624,-67 ]},
        {"from":"w2", "to":"310101", "points":[ -206.5,-52,-216.5,-52,-220,-52,-220,-188.005078125,-336.5,-188.005078125,-346.5,-188.005078125 ], "stroke":"#0af7f2"},
        {"from":"w2", "to":"310104", "points":[ -206.5,-52,-216.5,-52,-220,-52,-220,-90,-336.5,-90,-346.5,-90 ]},
        {"from":"w2", "to":"310105", "points":[ -206.5,-52,-216.5,-52,-220,-52,-220,10.994921875000003,-336.5,10.994921875000003,-346.5,10.994921875000003 ]},
        {"from":"w2", "to":"310106", "points":[ -184,-37,-184,-27,-184,-28,-184,-28,-184,68,-369,68,-369,79.994921875,-369,89.994921875 ]},
        {"from":"310101", "to":"w2", "category":"dotted", "points":[ -358.1372969866237,-203.005078125,-306,-275,-176,-275,-183.46188340807174,-67 ]},
        {"from":"310104", "to":"w2", "category":"dotted", "points":[ -354.96817076287164,-75,-309.8935685276608,-26.81533197830364,-263.2269018609942,-17.22974639271805,-206.5,-42.125441134420925 ], "stroke":"#1600ff"}
      ]
    }}
/>复制代码

节点之间建立关系,通过from和to来完成,这两个属性必填,其他字段都选填。另外配置里还有许多位置信息,比如point、location、segmentOffset等,不必担心,这些参数只是提供一个口子,用来精细化初始化,不是必填,如果没有这些信息,画布中会使用默认的layout来布局,并不会出现错乱情况。并且在画布上通过拖拽排版之后,组件会自动记录这些位置信息,第二次渲染就是使用这些信息来渲染,保证排版布局与用户的输入保持一致。

节点元素

<DiagramCanvasPanel
  dataSource={{
    "nodeDataArray": [
      {"key":"apple", "text":"苹果", "category": "apple", "location":"10 50"},
      {"key":"Actor", "text":"演员", "category": "Actor", "location":"200 50"},
      {"key":"test", "text":"自定义", "category": "test", "src": "https://img.alicdn.com/tfs/TB1lC5RCpYqK1RjSZLeXXbXppXa-80-80.png", "location":"100 200"}
    ]
  }}
/>
复制代码
Copy

节点元素可以通过"category"指定模版,既可以选择内置的模版,也可以使用自定义模版。如果不指定category或者category为'',则会使用默认模版,默认模版需要填写src。

内置节点模版:

注册自定义节点模版:

<DiagramCanvasPanel
  diagramNodeTemplateRegister={($, diagram) => {
    return [{
      name: 'test',
      template: $(go.Node, 'Vertical',
        new go.Binding('location', 'location', go.Point.parse).makeTwoWay(go.Point.stringify),
        new go.Binding('layerName', 'isSelected', function(s) {
          return s ? 'Foreground' : ''
        }).ofObject(),
        $(go.Picture, {
            portId: '',
            width: 80,
            height: 80,
            fromLinkable: true,
            toLinkable: true,
            fromLinkableSelfNode: true,
            toLinkableSelfNode: true,
            fromLinkableDuplicates: true,
            toLinkableDuplicates: true,
            cursor: 'pointer',
            scale: 0.5
          },
          new go.Binding('source', 'src'),
          new go.Binding('width', 'width'),
          new go.Binding('height', 'height'),
          new go.Binding('background', 'background'),
          new go.Binding('scale', 'scale')
        ),
        $(go.TextBlock, {
          margin: 10,
          maxSize: new go.Size(200, NaN),
          wrap: go.TextBlock.WrapFit,
          textAlign: 'center',
          font: 'bold 9pt Helvetica, Arial, sans-serif',
          cursor: 'move'
        },
        new go.Binding('text', 'text'),
        new go.Binding('font', 'font'),
        new go.Binding('margin', 'margin'),
        new go.Binding('textAlign', 'textAlign')
      ))
    }]
  }}
/>复制代码

自定义模版语法请参考gojs文档。

线条元素

<DiagramCanvasPanel
  dataSource={{
    "nodeDataArray": [
      {"key":"1", "text":"苹果", "category": "apple", "location":"10 50"},
      {"key":"2", "text":"演员", "category": "Actor", "location":"200 50"},
      {"key":"3", "text":"安卓", "category": "android", "location":"100 180"}
    ],
    "linkDataArray": [
      {"from":"1", "to":"2", "category":"Herringbone"},
      {"from":"2", "to":"1", "category":"Zipper"},
      {"from":"3", "to":"1", "category": "test"},
      {"from":"3", "to":"2", "category": "test"}
    ]
  }}
/>
复制代码
Copy

同样的,线条元素也可以通过"category"指定类型,也提供了许多类型可供选择,也可以组册自定义类型。如果不指定category或者category为'',则会使用默认模版,默认模版是黑色单线条。

内置线条模版:

注册自定义线条模版:

<DiagramCanvasPanel
  diagramLinkTemplateRegister={($, diagram) => {
    return [{ // 可以一次定义多个模版
      name: 'test', // 模版名称
      template: $(go.Link, { // 模版内容
        routing: go.Link.AvoidsNodes,
        curve: go.Link.JumpOver,
        relinkableFrom: true,
        relinkableTo: true,
        reshapable: true,
        resegmentable: true,
        toShortLength: 7,
        corner: 2
      },
      new go.Binding('points').makeTwoWay(),
      $(go.Shape,
        new go.Binding('stroke', 'stroke'),
        new go.Binding('strokeWidth', 'strokeWidth')
      ),
      $(go.Shape, { toArrow: 'Triangle' },
        new go.Binding('fill', 'stroke'),
        new go.Binding('stroke', 'stroke'),
        new go.Binding('strokeWidth', 'strokeWidth')
      ))
    }]
  }}
/>
复制代码
Copy

自定义模版语法请参考gojs文档。

除了画布内部元素的直接交互,画布元素还可以与业务表单进行交互,比如:我们封装的含有业务逻辑的仓库组件、区域组件、关系组件。

仓库组件

区域组件

关系组件

最后附一张零售通真实的仿真界面:

未来已来

随着仿真系统落地场景的增多,后面也会陆续沉淀更多相关的UI组件和垂直领域的业务组件,也欢迎有相似需求的同学一起共建。