AntV 开发 —— x6 图编辑引擎 v2

前文再续

前序:

碎碎念:

  • 此文个人笔记,官网文档搬运
  • x6 版本:2.9.7
  • x6-vue-shape 版本:2.0.11
  • x6-plugin-selection 版本:2.1.7
  • vue 版本:2.6.11
  • vue-template-compiler 版本:2.6.11
  • Element-UI 版本:2.15.13
  • 示例只展示单个节点的,多节点请下载demo自行查看源码
  • demo均在易水GIT

一:步骤:

1 —— 创建vue2项目:详情请看 vue开发 —— CLI(开发环境)搭建

2 —— 引入开发组件【Element-UI、antv.x6】

npm i element-ui
npm install @antv/x6 
npm install @antv/x6-vue-shape
npm install @antv/x6-plugin-selection 

2.1 —— 生成的项目目录如下:

Demo 
├─ node_modules
├─ public
     ├─ favicon.ico
     └─ index.html
├─ src
     ├─ assets
           └─ logo.png
     ├─ components
           └─ HelloWorld.vue
     ├─ App.vue
     └─ main.js
├─ .browserslistrc
├─ .eslintrc.js
├─  babel.config.js
├─ package.json
├─ package-lock.json
└─ README.md

2.2 —— 修改生成的项目【编辑main.js、编辑App.vue、新增Dag.vue、OneNode.vue】

// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

import '@/assets/base.scss';

Vue.use(ElementUI);

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');

// App.vue
<template>
  <div id="app">
    <div id="nav">
      <el-menu
        :default-active="activeIndex"
        class="el-menu-demo"
        mode="horizontal"
        :router="true"
      >
        <el-menu-item index="/">单个节点</el-menu-item>
        <el-menu-item index="/two">两个节点</el-menu-item>
        <el-menu-item index="/more">多节点</el-menu-item>
      </el-menu>
    </div>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      activeIndex: '/',
    };
  },
};
</script>
// Dag.vue
<template>
  <div class="data-processing-dag-node">
    <div
      class="main-area"
      @mouseenter="onMainMouseEnter"
      @mouseleave="onMainMouseLeave"
    >
      <div class="main-info">
        <!-- 节点类型icon -->
        <i
          class="node-logo"
          :style="{
            backgroundImage: 'url(' + global.NODE_TYPE_LOGO[type] + ')',
          }"
        />
        <el-tooltip :content="name" placement="top" :open-delay="800">
          <div class="ellipsis-row node-name">{{ name }}</div>
        </el-tooltip>
      </div>

      <!-- 节点状态信息 -->
      <div class="status-action">
        <el-tooltip
          :content="statusMsg"
          v-if="status === global.CellStatus.ERROR"
          placement="top"
        >
          <i class="status-icon status-icon-error" />
        </el-tooltip>
        <i
          class="status-icon status-icon-success"
          v-if="status === global.CellStatus.SUCCESS"
        />
        <!-- 节点操作菜单 -->
        <div class="more-action-container">
          <i class="more-action" />
        </div>
      </div>
    </div>

    <!-- +号菜单 -->
    <div class="plus-dag" v-if="type !== global.NodeType.OUTPUT">
      <el-dropdown trigger="click">
        <i class="plus-action" />
        <!-- <i class="el-icon-circle-plus-outline el-icon--right"></i> -->
        <el-dropdown-menu
          slot="dropdown"
          placement="bottom"
          class="processing-node-menu"
        >
          <el-dropdown-item
            v-for="(item, index) in global.PROCESSING_TYPE_LIST"
            :key="index"
          >
            <div
              class="node-dropdown-item"
              @click="clickPlusDragMenu(item.type)"
            >
              <i
                class="node-mini-logo"
                :style="{
                  backgroundImage: `url(${global.NODE_TYPE_LOGO[item.type]})`,
                }"
              />
              {{ item.name }}
            </div>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import dagDictionary from './dagDictionary';

export default {
  inject: ['getNode'],

  data() {
    return {
      name: '',
      status: '',
      statusMsg: '',
      type: '',
    };
  },

  mounted() {
    let node = this.getNode();
    // 初始化数据绑定
    this.mapper(node.data, this.$data);
    // 节点数据变化监听,从而绑定数据
    node.on('change:data', ({ current }) => this.mapper(current, this.$data));
  },

  methods: {
    mapper(source, target) {
      for (let key in target) {
        target[key] = source[key] ?? target[key];
      }
    },

    // 鼠标进入矩形主区域的时候显示连接桩
    onMainMouseEnter() {
      let node = this.getNode();
      // 获取该节点下的所有连接桩
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: '#fff',
          stroke: '#85A5FF',
        });
      });
    },

    // 鼠标离开矩形主区域的时候隐藏连接桩
    onMainMouseLeave() {
      let node = this.getNode();
      // 获取该节点下的所有连接桩
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: 'transparent',
          stroke: 'transparent',
        });
      });
    },

    // 点击添加下游+号
    clickPlusDragMenu(type) {
      this.createDownstream(type);
      this.setState({
        plusActionSelected: false,
      });
    },

    // 创建下游的节点和边
    createDownstream(type) {
      let node = this.getNode();
      const { graph } = node.model || {};
      if (graph) {
        // 获取下游节点的初始位置信息
        const position = this.getDownstreamNodePosition(node, graph);
        // 创建下游节点
        const newNode = this.createNode(type, graph, position);
        const source = node.id;
        const target = newNode.id;
        // 创建该节点出发到下游节点的边
        this.createEdge(source, target, graph);
      }
    },

    /**
     * 创建边并添加到画布
     * @param source
     * @param target
     * @param graph
     */
    createEdge(source, target, graph) {
      const edge = {
        id: this.uuid(),
        shape: 'data-processing-curve',
        source: {
          cell: source,
          port: `${source}-out`,
        },
        target: {
          cell: target,
          port: `${target}-in`,
        },
        zIndex: -1,
        data: {
          source,
          target,
        },
      };
      if (graph) {
        graph.addEdge(edge);
      }
    },

    /**
     * 创建节点并添加到画布
     * @param type 节点类型
     * @param graph
     * @param position 节点位置
     * @returns
     */
    createNode(type, graph, position) {
      if (!graph) {
        return {};
      }
      let newNode = {};
      const sameTypeNodes = graph
        .getNodes()
        .filter((item) => item.getData()?.type === type);
      const typeName = this.global.PROCESSING_TYPE_LIST?.find(
        (item) => item.type === type
      )?.name;
      const id = this.uuid();
      const node = {
        id,
        shape: 'data-processing-dag-node',
        x: position?.x,
        y: position?.y,
        ports: this.getPortsByType(type, id),
        data: {
          name: `${typeName}_${sameTypeNodes.length + 1}`,
          type,
        },
      };
      newNode = graph.addNode(node);
      return newNode;
    },

    /**
     * 根据起点初始下游节点的位置信息
     * @param node 起始节点
     * @param graph
     * @returns
     */
    getDownstreamNodePosition(node, graph, dx = 250, dy = 100) {
      // 找出画布中以该起始节点为起点的相关边的终点id集合
      const downstreamNodeIdList = [];
      graph.getEdges().forEach((edge) => {
        const originEdge = edge.toJSON()?.data;
        if (originEdge.source === node.id) {
          downstreamNodeIdList.push(originEdge.target);
        }
      });
      // 获取起点的位置信息
      const position = node.getPosition();
      let minX = Infinity;
      let maxY = -Infinity;
      graph.getNodes().forEach((graphNode) => {
        if (downstreamNodeIdList.indexOf(graphNode.id) > -1) {
          const nodePosition = graphNode.getPosition();
          // 找到所有节点中最左侧的节点的x坐标
          if (nodePosition.x < minX) {
            minX = nodePosition.x;
          }
          // 找到所有节点中最x下方的节点的y坐标
          if (nodePosition.y > maxY) {
            maxY = nodePosition.y;
          }
        }
      });

      return {
        x: minX !== Infinity ? minX : position.x + dx,
        y: maxY !== -Infinity ? maxY + dy : position.y,
      };
    },

    // 根据节点的类型获取ports
    getPortsByType(type, nodeId) {
      let ports = [];
      switch (type) {
        case this.global.NodeType.INPUT:
          ports = [
            {
              id: `${nodeId}-out`,
              group: 'out',
            },
          ];
          break;
        case this.global.NodeType.OUTPUT:
          ports = [
            {
              id: `${nodeId}-in`,
              group: 'in',
            },
          ];
          break;
        default:
          ports = [
            {
              id: `${nodeId}-in`,
              group: 'in',
            },
            {
              id: `${nodeId}-out`,
              group: 'out',
            },
          ];
          break;
      }
      return ports;
    },

    uuid() {
      var s = [];
      var hexDigits = '0123456789abcdef';
      for (var i = 0; i < 32; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
      }
      s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010
      s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
      s[8] = s[13] = s[18] = s[23];
      var uuid = s.join('');
      return uuid;
    },
  },

  computed: {
    global: function () {
      return dagDictionary;
    },
  },
};
</script>

// OneNode.vue
<template>
  <div>
    <el-alert
      title="温馨提示"
      type="success"
      description="单个演示,不涉及链接,仅就描绘单个节点的场景"
    >
    </el-alert>
    <div id="oneNode"></div>
  </div>
</template>

<script>
import { Graph } from '@antv/x6';
import { register } from '@antv/x6-vue-shape';
import DagNode from '@/components/dag/Index';
import dagMap from '@/components/dag/dagMap.json';

export default {
  data() {
    return {
      // 节点状态列表
      nodeStatusList: [
        {
          id: 'node-0',
          status: 'success',
        },
        {
          id: 'node-1',
          status: 'success',
        },
        {
          id: 'node-2',
          status: 'success',
        },
        {
          id: 'node-3',
          status: 'success',
        },
        {
          id: 'node-4',
          status: 'error',
          statusMsg: '错误信息示例',
        },
      ],
    };
  },

  mounted() {
    // 1. 注册节点
    register({
      shape: 'data-processing-dag-node',
      width: 212,
      height: 48,
      component: DagNode,
    });

    // 2. 创建画布
    const graph = new Graph({
      container: document.getElementById('oneNode'),
      width: 1000,
      height: 1000,
    });

    // 3. 根据json数据创建节点,此处只取第一个
    let map = {};
    map.nodes = dagMap.nodes.slice(0, 1);
    graph.fromJSON(map);

    // 4. 设置节点状态
    let { id, status, statusMsg } = this.nodeStatusList[0];
    let node = graph.getCellById(id);
    let data = node.getData();
    node.setData({
      ...data,
      status,
      statusMsg,
    });
  },
};
</script>
单节点
两节点

多节点

2.3 —— 收工,翻归卖豉油

二:常见问题:

暂未发现

三:吹水:

1 这个版本比版本一要友好了很多,特别是在vue的处理上,但是很可惜目前很多demo均是基于react的,但是不影响vue的用户,以上面的列子来说,主要就是注册模块、创建画布、创建节点基本步骤即可,其他都是和以前怎么操作就怎么操作,不管是哪个框架,这次用v2重写一遍,相对于写v1理顺了很多,3个小demo,这种拆解官方demo的方式不一定让你得到什么,demo本就没什么可说的内容,但是希望能让你看清,这个是怎么和框架脱钩又和框架组合的,在处理需求时,看清需求,用对方式,而不是硬套框架这些

2 至于版本1和2如何抉择,建议是能上版本2不要犹豫,版本一的教程可以不看了,但是相比这次版本更新,这个版本2还是存在一些问题,并没达到很好的效果,拖动也不够流畅,有些东西还是可以封装下,减少重复劳动

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容