AntV 開(kāi)發(fā) —— x6 圖編輯引擎 v2

前文再續(xù)

前序:

碎碎念:

  • 此文個(gè)人筆記鸽疾,官網(wǎng)文檔搬運(yùn)
  • 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
  • 示例只展示單個(gè)節(jié)點(diǎn)的,多節(jié)點(diǎn)請(qǐng)下載demo自行查看源碼
  • demo均在易水GIT

一:步驟:

1 —— 創(chuàng)建vue2項(xiàng)目:詳情請(qǐng)看 vue開(kāi)發(fā) —— CLI(開(kāi)發(fā)環(huán)境)搭建

2 —— 引入開(kāi)發(fā)組件【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 —— 生成的項(xiàng)目目錄如下:

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 —— 修改生成的項(xiàng)目【編輯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="/">單個(gè)節(jié)點(diǎn)</el-menu-item>
        <el-menu-item index="/two">兩個(gè)節(jié)點(diǎn)</el-menu-item>
        <el-menu-item index="/more">多節(jié)點(diǎn)</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">
        <!-- 節(jié)點(diǎn)類(lèi)型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>

      <!-- 節(jié)點(diǎn)狀態(tài)信息 -->
      <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"
        />
        <!-- 節(jié)點(diǎn)操作菜單 -->
        <div class="more-action-container">
          <i class="more-action" />
        </div>
      </div>
    </div>

    <!-- +號(hào)菜單 -->
    <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();
    // 初始化數(shù)據(jù)綁定
    this.mapper(node.data, this.$data);
    // 節(jié)點(diǎn)數(shù)據(jù)變化監(jiān)聽(tīng)吼和,從而綁定數(shù)據(jù)
    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];
      }
    },

    // 鼠標(biāo)進(jìn)入矩形主區(qū)域的時(shí)候顯示連接樁
    onMainMouseEnter() {
      let node = this.getNode();
      // 獲取該節(jié)點(diǎn)下的所有連接樁
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: '#fff',
          stroke: '#85A5FF',
        });
      });
    },

    // 鼠標(biāo)離開(kāi)矩形主區(qū)域的時(shí)候隱藏連接樁
    onMainMouseLeave() {
      let node = this.getNode();
      // 獲取該節(jié)點(diǎn)下的所有連接樁
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: 'transparent',
          stroke: 'transparent',
        });
      });
    },

    // 點(diǎn)擊添加下游+號(hào)
    clickPlusDragMenu(type) {
      this.createDownstream(type);
      this.setState({
        plusActionSelected: false,
      });
    },

    // 創(chuàng)建下游的節(jié)點(diǎn)和邊
    createDownstream(type) {
      let node = this.getNode();
      const { graph } = node.model || {};
      if (graph) {
        // 獲取下游節(jié)點(diǎn)的初始位置信息
        const position = this.getDownstreamNodePosition(node, graph);
        // 創(chuàng)建下游節(jié)點(diǎn)
        const newNode = this.createNode(type, graph, position);
        const source = node.id;
        const target = newNode.id;
        // 創(chuàng)建該節(jié)點(diǎn)出發(fā)到下游節(jié)點(diǎn)的邊
        this.createEdge(source, target, graph);
      }
    },

    /**
     * 創(chuàng)建邊并添加到畫(huà)布
     * @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);
      }
    },

    /**
     * 創(chuàng)建節(jié)點(diǎn)并添加到畫(huà)布
     * @param type 節(jié)點(diǎn)類(lèi)型
     * @param graph
     * @param position 節(jié)點(diǎn)位置
     * @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;
    },

    /**
     * 根據(jù)起點(diǎn)初始下游節(jié)點(diǎn)的位置信息
     * @param node 起始節(jié)點(diǎn)
     * @param graph
     * @returns
     */
    getDownstreamNodePosition(node, graph, dx = 250, dy = 100) {
      // 找出畫(huà)布中以該起始節(jié)點(diǎn)為起點(diǎn)的相關(guān)邊的終點(diǎn)id集合
      const downstreamNodeIdList = [];
      graph.getEdges().forEach((edge) => {
        const originEdge = edge.toJSON()?.data;
        if (originEdge.source === node.id) {
          downstreamNodeIdList.push(originEdge.target);
        }
      });
      // 獲取起點(diǎn)的位置信息
      const position = node.getPosition();
      let minX = Infinity;
      let maxY = -Infinity;
      graph.getNodes().forEach((graphNode) => {
        if (downstreamNodeIdList.indexOf(graphNode.id) > -1) {
          const nodePosition = graphNode.getPosition();
          // 找到所有節(jié)點(diǎn)中最左側(cè)的節(jié)點(diǎn)的x坐標(biāo)
          if (nodePosition.x < minX) {
            minX = nodePosition.x;
          }
          // 找到所有節(jié)點(diǎn)中最x下方的節(jié)點(diǎn)的y坐標(biāo)
          if (nodePosition.y > maxY) {
            maxY = nodePosition.y;
          }
        }
      });

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

    // 根據(jù)節(jié)點(diǎn)的類(lèi)型獲取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="單個(gè)演示一屋,不涉及鏈接窘疮,僅就描繪單個(gè)節(jié)點(diǎn)的場(chǎng)景"
    >
    </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 {
      // 節(jié)點(diǎn)狀態(tài)列表
      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: '錯(cuò)誤信息示例',
        },
      ],
    };
  },

  mounted() {
    // 1. 注冊(cè)節(jié)點(diǎn)
    register({
      shape: 'data-processing-dag-node',
      width: 212,
      height: 48,
      component: DagNode,
    });

    // 2. 創(chuàng)建畫(huà)布
    const graph = new Graph({
      container: document.getElementById('oneNode'),
      width: 1000,
      height: 1000,
    });

    // 3. 根據(jù)json數(shù)據(jù)創(chuàng)建節(jié)點(diǎn),此處只取第一個(gè)
    let map = {};
    map.nodes = dagMap.nodes.slice(0, 1);
    graph.fromJSON(map);

    // 4. 設(shè)置節(jié)點(diǎn)狀態(tài)
    let { id, status, statusMsg } = this.nodeStatusList[0];
    let node = graph.getCellById(id);
    let data = node.getData();
    node.setData({
      ...data,
      status,
      statusMsg,
    });
  },
};
</script>
單節(jié)點(diǎn)
兩節(jié)點(diǎn)

多節(jié)點(diǎn)

2.3 —— 收工冀墨,翻歸賣(mài)豉油

二:常見(jiàn)問(wèn)題:

暫未發(fā)現(xiàn)

三:吹水:

1 這個(gè)版本比版本一要友好了很多,特別是在vue的處理上诽嘉,但是很可惜目前很多demo均是基于react的蔚出,但是不影響vue的用戶(hù),以上面的列子來(lái)說(shuō)虫腋,主要就是注冊(cè)模塊骄酗、創(chuàng)建畫(huà)布、創(chuàng)建節(jié)點(diǎn)基本步驟即可悦冀,其他都是和以前怎么操作就怎么操作趋翻,不管是哪個(gè)框架,這次用v2重寫(xiě)一遍盒蟆,相對(duì)于寫(xiě)v1理順了很多踏烙,3個(gè)小demo,這種拆解官方demo的方式不一定讓你得到什么历等,demo本就沒(méi)什么可說(shuō)的內(nèi)容讨惩,但是希望能讓你看清,這個(gè)是怎么和框架脫鉤又和框架組合的寒屯,在處理需求時(shí)荐捻,看清需求,用對(duì)方式,而不是硬套框架這些

2 至于版本1和2如何抉擇靴患,建議是能上版本2不要猶豫仍侥,版本一的教程可以不看了,但是相比這次版本更新鸳君,這個(gè)版本2還是存在一些問(wèn)題农渊,并沒(méi)達(dá)到很好的效果,拖動(dòng)也不夠流暢或颊,有些東西還是可以封裝下砸紊,減少重復(fù)勞動(dòng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市囱挑,隨后出現(xiàn)的幾起案子醉顽,更是在濱河造成了極大的恐慌,老刑警劉巖平挑,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件游添,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡通熄,警方通過(guò)查閱死者的電腦和手機(jī)唆涝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)唇辨,“玉大人廊酣,你說(shuō)我怎么就攤上這事∩兔叮” “怎么了亡驰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)饿幅。 經(jīng)常有香客問(wèn)我凡辱,道長(zhǎng),這世上最難降的妖魔是什么栗恩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任煞茫,我火速辦了婚禮,結(jié)果婚禮上摄凡,老公的妹妹穿的比我還像新娘续徽。我一直安慰自己,他們只是感情好亲澡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布钦扭。 她就那樣靜靜地躺著,像睡著了一般床绪。 火紅的嫁衣襯著肌膚如雪客情。 梳的紋絲不亂的頭發(fā)上其弊,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音膀斋,去河邊找鬼梭伐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛仰担,可吹牛的內(nèi)容都是我干的糊识。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼摔蓝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赂苗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起贮尉,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拌滋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后猜谚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體败砂,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年魏铅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昌犹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沦零,死狀恐怖祭隔,靈堂內(nèi)的尸體忽然破棺而出货岭,到底是詐尸還是另有隱情路操,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布千贯,位于F島的核電站屯仗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搔谴。R本人自食惡果不足惜魁袜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望敦第。 院中可真熱鬧峰弹,春花似錦、人聲如沸芜果。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)右钾。三九已至蚁吝,卻和暖如春旱爆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窘茁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工怀伦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人山林。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓房待,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親捌朴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吴攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容