[Простая версия] Интерфейсная визуализация направленного ациклического графа (DAG)

контейнер

首发简书, 此为合并整理版.代码链接放在文末

В последнее время компании необходимо построить платформу машинного обучения для внутреннего использования, часть требований можно абстрагировать в направленный ациклический граф, а процесс НИОКР записывать пока ступаю на яму (фактически готовое колесо сильносцепленного бизнес не может быть найден 🤷), Если у вас есть подобные потребности, вы можете также замочить чашку годжи и медленно читать эту статью.

Содержание обучающей реализации:

Перетащите узел модели, установите взаимосвязь (соединение)

Внешние операции узла модели (добавление и удаление узла, обнаружение кольца DAG, реализованное внешним интерфейсом)

Плоскостное перемещение всего изображения модели (полный масштаб изображения, выделение, полный экран и т. д.)

Выбор технологии для фронтальной визуализации.

Для первоначальных требований к подключению рассмотрите возможность использования svg и canvas для комплексного достижения этого содержимого:

имя svg canvas
Качество изображения Свободно масштабируйте вектор Растровое изображение, искажение масштаба
управляемый событиями Основываясь на элементах dom, события привязки легко Управляемая сценарием конфигурация событий не является гибкой
представление То же, что и выше, поэтому слишком много элементов рендеринга вызовет отставание. Чрезвычайно высокая производительность и будущая тенденция закадрового холста
Применимая сцена Интерактивное поведение с меньшим количеством изображений на несколько порядков Рендеринг супер повторяющихся элементов
стоимость обучения относительно простой Есть стоимость, чтобы начать

Поэтому svg используется как единое целое, и на рынке есть много готовых продуктов на основе svg, таких как ink knife, processon, noflo и множество платформ Али, которые неплохо себя показывают в некоторых сценариях (конечно, это также удобно открыть код обучения в любое время.напишите его~)

Книга продолжает предыдущую статью и возвращается к теме

Во-первых, реализация узлов

Соответствующий код узла (начальная версия)

{
    name: "name1",
    description: "description1",
    id: 1,
    parentNode: 0,
    childNode: 2,
    imgContent: "",
    parentDetails: {
      a: "",
      b: ""
    },
    linkTo: [{ id: 2 }, { id: 3 }],
    translate: {
      left: 100,
      top: 20
    }
  }

Более поздний этап (после шага 5 руководства) оптимизирован как:

{
    name: "name2",
    id: 2,
    imgContent: "",
    pos_x: 300,
    pos_y: 400,
    type: 'constant',
    in_ports: [0, 1, 2, 3, 4],
    out_ports: [0, 1, 2, 3, 4]
 }

Пожалуйста, не обращайте внимания на абстракцию составителя душ, все управляется данными, а узлам модели нужно только взаимодействовать с серверными исследованиями и разработками, имитируя приведенную выше диаграмму.

Во-вторых, реализация модели узлового соединения

<path
  class="connector"
  v-for="(each, n) in item.linkTo" :key="n"
  :d="computedLink(i, each, n)">
</path>

Основываясь на реализации vue, он напрямую используется: d Динамический расчет кривой Безье, Идея состоит в том, чтобы использовать идентификатор узлов входа и выхода для расчета начальной позиции и назначения формулы кривойНажмите -> о кривых Безье для справкиhttps://brucewar.gitbooks.io/svg-tutorial/15.SVG-path%E5%85%83%E7%B4%A0.html

В-третьих, реализация перетаскивания узла

dragPre(e, i) {
      // 准备拖动节点
      this.setInitRect(); // 初始化画板坐标
      this.currentEvent = "dragPane"; // 修正行为
      this.choice.index = i;
      this.setDragFramePosition(e);
    },

Причина инициализации артборда: Поскольку положение элемента в окне не фиксировано, каждый раз требуются начальные координаты, что удобно для расчета относительного смещения.

<g
        :transform="`translate(${dragFrame.posX}, ${dragFrame.posY})`"
        class="dragFrame">
          <foreignObject width="180" height="30" >
            <body xmlns="http://www.w3.org/1999/xhtml">
              <div
              v-show="currentEvent === 'dragPane'"
              class="dragFrameArea">
              </div>
            </body>
          </foreignObject>
        </g>

mousedown时获取拖拽元素的下标,修正坐标

dragIng(e) {
      if (this.currentEvent === "dragPane") {
        this.setDragFramePosition(e);
        // 模拟框随动
      }
    },
setDragFramePosition(e) {
      const x = e.x - this.initPos.left; // 修正拖动元素坐标
      const y = e.y - this.initPos.top;
      this.dragFrame = { posX: x - 90, posY: y - 15 };
    }

拖动时给模拟拖动的元素赋值位置

dragEnd(e) {
      // 拖动结束
      if (this.currentEvent === "dragPane") {
        this.dragFrame = { dragFrame: false, posX: 0, posY: 0 };
        this.setPanePosition(e); // 设定拖动后的位置
      }
      this.currentEvent = null; // 清空事件行为
    },
setPanePosition(e) {
      const x = e.x - this.initPos.left - 90;
      const y = e.y - this.initPos.top - 15;
      const i = this.choice.index;
      this.DataAll[i].translate = { left: x, top: y };
    },

拖动结束把新的位置赋值给对应元素,当然在实际项目中, 每次变更需要跟后台交互这些数据, 不需要前端模拟数据变更的,直接请求整张图的接口重新渲染就好了,更easy

В-четвертых, реализация перетаскивания узловых соединений

Как и в предыдущем шаге, мы также прослушиваем события mousedown, mousemove и mouseup, чтобы добиться эффекта перетаскивания соединения между узлами.Единственная трудность заключается в вычислении начальной позиции.

<g>
          <path
          class="connector"
          :d="dragLinkPath()"
          ></path>
        </g>

首先来个path

setInitRect() {
      let { left, top } = document
        .getElementById("svgContent")
        .getBoundingClientRect();
      this.initPos = { left, top }; // 修正坐标
    },
    linkPre(e, i) {
      this.setInitRect();
      this.currentEvent = "dragLink";
      this.choice.index = i;
      this.setDragLinkPostion(e, true);
      e.preventDefault();
      e.stopPropagation();
    },

mousedown修正坐标

dragIng(e) {
      if (this.currentEvent === "dragLink") {
        this.setDragLinkPostion(e);
      }
    },

mousemove的时候确定位置

linkEnd(e, i) {
      if (this.currentEvent === "dragLink") {
        this.DataAll[this.choice.index].linkTo.push({ id: i });
        this.DataAll.find(item => item.id === i).parentNode = 1;
      }
      this.currentEvent = null;
    },
    setDragLinkPostion(e, init) {
      // 定位连线
      const x = e.x - this.initPos.left;
      const y = e.y - this.initPos.top;
      if (init) {
        this.dragLink = Object.assign({}, this.dragLink, {
          fromX: x,
          fromY: y
        });
      }
      this.dragLink = Object.assign({}, this.dragLink, { toX: x, toY: y });
    },

mouseup的时候判断连入了哪个元素

5. Объедините вышеуказанные шаги и разделите компоненты

По мере увеличения контента нам необходимо интегрировать весь контент и разделять компоненты на основе связанного контента.Подробности см. в структуре каталогов.

Все соединения становятся компонентами стрелки и наследуют только положение координат для рендеринга. симулироватьFrame и симулироватьАрроу только динамически наследуют координаты при перетаскивании, чтобы имитировать эффект перетаскивания

6. Реализация перетаскивания и добавления узла

С процессно-ориентированной точки зрения перетаскивание узлов — это не что иное, как три операции:

·Перед перетаскиванием判断当前情况下能否拖动, 拖动的元素携带的节点类型,节点名称等参数

·Тянуть模拟的节点随鼠标进行位移,将参数赋值给模拟的节点

· Перетаскивание判断松手位置是否在画板中, ( 更改模型数据 | 调用后台接口 )

Итак, нам нужен фиктивный элемент, который может перемещаться на весь экран.如图 class='nodesBus-contain'

<nodes-bus v-if="dragBus" :value="busValue.value" :pos_x="busValue.pos_x" :pos_y="busValue.pos_y" />

Этот элемент располагается рядом с самым большим контейнером в глобальной модели DOM и получает координаты положения и отображаемое имя.

      dragBus: false,
      busValue: {
        value: "name",
        pos_x: 100,
        pos_y: 100
      }

Самый внешний компонент использует dragBus для управления отображением, положением и т. д.

  <div class="page-content" @mousedown="startNodesBus($event)" @mousemove="moveNodesBus($event)" @mouseup="endNodesBus($event)">

Внешний контейнер три события, mouseDown, mouseMove, mouseUp

<span @mousedown="dragIt('拖动1')">拖动我吧1</span>
<span @mousedown="dragIt('拖动2')">拖动我吧2</span>
dragIt(val) {
      sessionStorage["dragDes"] = JSON.stringify({
        drag: true,
        name: val
      });
    }

Элементы, которые необходимо щелкнуть, чтобы вызвать перетаскивание, используют кеш для передачи данных и управления имитируемыми узлами.

startNodesBus(e) {
      /**
       *  别的组件调用时, 先放入缓存
       * dragDes: {
       *    drag: true,
       *    name: 组件名称
       *    type: 组件类型
       *    model_id: 跟后台交互使用
       * }
       **/
      let dragDes = null;
      if (sessionStorage["dragDes"]) {
        dragDes = JSON.parse(sessionStorage["dragDes"])
      }
      if (dragDes && dragDes.drag) {
        const x = e.pageX;
        const y = e.pageY;
        this.busValue = Object.assign({}, this.busValue, {
          pos_x: x,
          pos_y: y,
          value: dragDes.name
        });
        this.dragBus = true;
      }
    }

Когда он перемещается к верхнему компоненту, запускается событие mouseUp контейнера, отображается смоделированный узел и назначаются необходимые параметры.使用缓存来控制行为,是为了防止别的无关元素干扰.

moveNodesBus(e) {
      if (this.dragBus) {
        const x = e.pageX;
        const y = e.pageY;
        this.busValue = Object.assign({}, this.busValue, {
          pos_x: x,
          pos_y: y
        });
      }
    },

Поведение перемещения очень простое, вам нужно только динамически назначать позицию мыши на странице для входа.

endNodesBus(e) {
      let dragDes = null;
      if (sessionStorage["dragDes"]) {
        dragDes = JSON.parse(sessionStorage["dragDes"])
      }
      if (dragDes && dragDes.drag && e.toElement.id === "svgContent") {
        const { model_id, type } = dragDes;
        const pos_x = e.offsetX - 90; // 参数修正
        const pos_y = e.offsetY - 15; // 参数修正
        const params = {
          model_id: sessionStorage["newGraph"],
          desp: {
            type,
            pos_x,
            pos_y,
            name: this.busValue.value
          }
        };
        this.addNode(params);
      }
      window.sessionStorage["dragDes"] = null;
      this.dragBus = false;
    }

Выньте положение мыши, когда mouseUp, и измените данные модели после исправления, Вызванный здесь this.addNode(params) исходит из vuex, и позже vuex будет объяснен в унифицированном виде.

7. Удаление узла

Удаление узла использует щелчок правой кнопкой мыши, чтобы открыть окно параметров, где мы можем отслеживать поведение элемента при щелчке правой кнопкой мыши и отключать все поведения по умолчанию.

        <g
        v-for="(item, i) in DataAll.nodes"
        :key="'_' + i" class="svgEach"
        :transform="`translate(${item.pos_x}, ${item.pos_y})`"
        @contextmenu="r_click_nodes($event, i)">
---------------------------------------------------------------------------
  r_click_nodes(e, i) { // 节点的右键事件
      this.setInitRect()
      const id = this.DataAll.nodes[i].id;
      const x = e.x - this.initPos.left;
      const y = e.y - this.initPos.top;
      this.is_edit_area = {
        value: true,
        x,
        y,
        id
      }
      e.stopPropagation();
      e.cancelBubble = true;
      e.preventDefault();
    }

Затем передайте идентификатор узла и положение мыши операции компоненту имитации опции.nodesBus.vueчтобы убедиться, что поле параметра отображается в соответствующем положении. Здесь еще есть ямка.Нам нужно сделать так, чтобы нажатие на другие позиции могло закрыть модальное окно, поэтому нам нужно добавить слой маски.Здесь я пошел на хитрость и не добавил слой покрытия div

 <foreignObject width="100%" height="100%" style="position: relative" @click="click_menu_cover($event)">
        <body xmlns="http://www.w3.org/1999/xhtml" :style="get_menu_style()">
            <div class="menu_contain">
                <span @click="delEdges">删除节点</span>
                <span>编辑</span>
                <span>干点别的啥</span>
            </div>
        </body>
 </foreignObject>
-------------------------------------------------
click_menu_cover(e) {
      this.$emit('close_click_nodes')
      e.preventDefault();
      e.cancelBubble = true;
      e.stopPropagation();
 },

Просто перехватите mouseDown внутри компонента, чтобы закрыть всплывающее окно.

let params = {
        model_id: sessionStorage['newGraph'],
        id: this.isEditAreaShow.id
 }
this.delNode(params)

model_id是本项目跟后台交互的参数请无视Получите идентификатор и напрямую вызовите delNode vuex

Восемь, подключение, удаление узла и использование vuex

Чтобы разделить компоненты более подробно и облегчить обмен данными между компонентами, в качестве передачи данных этого проекта вводится vuex.Несколько компонентов используются вместе.dagStore.jsДанныеВсе,

    addEdge: ({ commit }, { desp }) => { // 增加边
      commit('ADD_EDGE_DATA', desp)
    },
    delEdge: ({ commit }, { id }) => { // 删除边
      commit('DEL_EDGE_DATA', id)
    },
    moveNode: ({ commit }, params) => { // 移动点的位置
      commit('MOVE_NODE_DATA', params)
    },
    addNode: ({ commit }, params) => { // 增加节点
      commit('ADD_NODE_DATA', params)
    },
    delNode: ({ commit }, { id }) => { // 删除节点
      commit('DEL_NODE_DATA', id)
    },

Структура данных состояния

DataAll: {
      nodes: [{
        name: "name5",
        id: 1,
        imgContent: "",
        pos_x: 100,
        pos_y: 230,
        type: "constant",
        in_ports: [0, 1, 2],
        out_ports: [0, 1, 2, 3, 4]
      }],
      edges: [{
        id: 1,
        dst_input_idx: 1,
        dst_node_id: 1,
        src_node_id: 2,
        src_output_idx: 2
      }],
      model_id: 21
    }

Все операции только изменяют DataAll в состоянии.

ADD_NODE_DATA: (state, params) => {
      let _nodes = state.DataAll.nodes
      _nodes.push({
        ...params.desp,
        id: state.DataAll.nodes.length + 10,
        in_ports: [0, 1, 2, 3, 4],
        out_ports: [0, 1, 2, 3, 4]
      })
}

новый узел

DEL_NODE_DATA: (state, id) => {
      let _edges = []
      let _nodes = []
      state.DataAll.edges.forEach(item => {
        if (item.dst_node_id !== id && item.src_node_id !== id) {
          _edges.push(item)
        }
      })
      state.DataAll.nodes.forEach(item => {
        if (item.id !== id) {
          _nodes.push(item)
        }
      })
      state.DataAll.edges = _edges
      state.DataAll.nodes = _nodes
 }

Удаление узла

DEL_EDGE_DATA: (state, id) => {
      let _edges = []
      state.DataAll.edges.forEach((item, i) => {
        if (item.id !== id) {
          _edges.push(item)
        }
      })
      state.DataAll.edges = _edges
},

Очистка соединений между узлами

ADD_EDGE_DATA: (state, desp) => {
      let _DataAll = state.DataAll
      _DataAll.edges.push({
        ...desp,
        id: state.DataAll.edges.length + 10
      })
      /**
       * 检测是否成环
       **/
      let isCircle = false
      const { dst_node_id } = desp // 出口 入口id
      const checkCircle = (dst_node_id, nth) => {
        if (nth > _DataAll.nodes.length) {
          isCircle = true
          return false
        } else {
          _DataAll.edges.forEach(item => {
            if (item.src_node_id === dst_node_id) {
              console.log('目标节点是', item.src_node_id, '次数为', nth)
              checkCircle(item.dst_node_id, ++nth)
            }
          })
        }
      }
      checkCircle(dst_node_id, 1)
      if (isCircle) {
        _DataAll.edges.pop()
        alert('禁止成环')
      }
}

Приведенный выше код представляет собой увеличение узлов, что добавляет обнаружение того, является ли это кольцом, Продолжайте рекурсию узлов и найдите путь к узлу от целевого узла.Если количество циклов превышает общее количество узлов, это доказывает, что цикл есть, и операция отменяется.

在实际项目中, 每一步操作都可以传给后端,因此前端没有很大计算量,由后端同学负责放在缓存中计算

Девять, реализация перетаскивания всей картины

Реализация перетаскивания всего изображения Поместите все изображение в элемент ag внутри svg и динамически передайте перевод преобразования в элемент g, чтобы изменить положение.Поскольку это значение состояния компонента (состояние), автор не рекомендует помещать его в vuex для контроля, и рекомендуя положить в vue Данных в компоненте достаточно.В этом проекте автор сохраняет sessionStorage, что удобно для точного расчета текущего положения мыши и положения мыши в исходном масштабе.

 svgMouseDown(e) {
      // svg鼠标按下触发事件分发
      this.setInitRect();
      if (this.currentEvent === "sel_area") {
        this.selAreaStart(e);
      } else {
        // 那就拖动画布
        this.currentEvent = "move_graph";
        this.graphMovePre(e);
      }
    },

事件触发: 在svg画布mousedown的时候进行事件分发

 /**
     * 画布拖动
     */
    graphMovePre(e) {
      const { x, y } = e;
      this.svg_trans_init = { x, y };
      this.svg_trans_pre = { x: this.svg_left, y: this.svg_top };
    },
    graphMoveIng(e) {
      const { x, y } = this.svg_trans_init;
      this.svg_left = e.x - x + this.svg_trans_pre.x;
      this.svg_top = e.y - y + this.svg_trans_pre.y;
      sessionStorage["svg_left"] = this.svg_left;
      sessionStorage["svg_top"] = this.svg_top;
    },

在mousemove的过程中监听鼠标动态变化, 通过比较mousedown的初始位置,来更改当前画布位置 关于坐标计算的问题放在整图缩放里讲, 回归坐标计算需要考虑缩放倍数

10. Реализация масштабирования всего изображения и текущего положения мыши для расчета исходных координат.

То же, что и одиннадцать, с помощью transform: scale(x) тега g под svg для выполнения общего масштабирования узла.

    <g :transform="` translate(${svg_left}, ${svg_top}) scale(${svgScale})`" >

Здесь svgScale использует vuex для управления, чтобы доказать, что не существует единой спецификации для управления состоянием компонентов, но все же настоятельно рекомендуется, чтобы состояние передавалось компонентам, а данные (данные) передавались vuex.
↓↓

    svgScale: state => state.dagStore.svgSize

Здесь добавлен новый компонент плавающей панели, с которым удобно работать пользователям.

<template>
     <g>
        <foreignObject width="200px" height="30px" style="position: relative">
        <body xmlns="http://www.w3.org/1999/xhtml">
            <div class="control_menu">
                <span @click="sizeExpend">╋</span>
                <span @click="sizeShrink">一</span>
                <span @click="sizeInit">╬</span>
                <span :class="['sel_area', 'sel_area_ing'].indexOf(currentEvent) !== -1 ? 'sel_ing' : ''" @click="sel_area($event)">口</span>
                <span @click="fullScreen">{{ changeScreen }}</span>
            </div>
        </body>
        </foreignObject>
    </g>
</template>
 /**
     *  svg画板缩放行为
     */
    sizeInit() {
      this.changeSize("init"); // 回归到默认倍数
      this.svg_left = 0; // 回归到默认位置
      this.svg_top = 0;
      sessionStorage['svg_left'] = 0;
      sessionStorage['svg_top'] = 0;
    },
    sizeExpend() {
      this.changeSize("expend"); // 画板放大0.1
    },
    sizeShrink() {
      this.changeSize("shrink"); // 画板缩小0.1
    },

Поскольку он управляется vuex, измените svgSize в мутации

CHANGE_SIZE: (state, action) => {
      switch (action) {
        case 'init':
          state.svgSize = 1
          break
        case 'expend':
          state.svgSize += 0.1
          break
        case 'shrink':
          state.svgSize -= 0.1
          break
        default: state.svgSize = state.svgSize
      }
      sessionStorage['svgScale'] = state.svgSize
    },

До настоящего времени мы завершили функции движущихся и масштабирования координат и масштабирования графика. Следующая важная проблема заключается в том, что, когда мы работаем поведение координат, мы можем получить только координаты в компоненте, что приведет к тому, что все результаты будут неуместны, Нам нужно пересчитать, чтобы вернуть реальные координаты без масштабирования и без смещения.

Возьмите конец перетаскивания узла в качестве примера

paneDragEnd(e) {
      // 节点拖动结束
      this.dragFrame = { dragFrame: false, posX: 0, posY: 0 }; // 关闭模态框
      const x = // x轴坐标需要减去X轴位移量, 再除以放缩比例 减去模态框宽度一半
        (e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) / this.svgScale -
        90;
      const y = // y轴坐标需要减去y轴位移量, 再除以放缩比例 减去模态框高度一半
        (e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) / this.svgScale -
        15;
      let params = {
        model_id: sessionStorage["newGraph"],
        id: this.DataAll.nodes[this.choice.index].id,
        pos_x: x,
        pos_y: y
      };
      this.moveNode(params);
    },

Для всех позиций, где используются координаты, необходимо вычесть смещение координат по горизонтали и вертикали, а затем разделить на коэффициент масштабирования, чтобы получить исходное соотношение. Код не будет повторяться.

Одиннадцать, полный экран

以chrome浏览器的为例, 不同浏览器都元素放缩有着不同的api

    fullScreen() {
      if (this.changeScreen === "全") {
        this.changeScreen = "关";
        let root = document.getElementById("svgContent");
        root.webkitRequestFullScreen();
      } else {
        this.changeScreen = "全";
        document.webkitExitFullscreen();
      }
    }

document.getElementById('svgContent').webkitRequestFullScreen() делает элемент полноэкранным. document.webkitExitFullScreen() выходит из полноэкранного режима.

12. Шатер с резинкой

Идея окна выбора резиновой ленты состоит в том, чтобы перетащить модальное поле div, получить координаты верхнего левого и нижнего правого и изменить состояние выбора узлов в пределах двух координат.

    <div :class="choice.paneNode.indexOf(item.id) !== -1 ? 'pane-node-content selected' : 'pane-node-content'">
      choice: {
        paneNode: [], // 选取的节点下标组
        index: -1,
        point: -1 // 选取的点数的下标
      },

Состояние выбора — это состояние компонента, поэтому оно находится под контролем компонента, без vuex.Выбору кадра нужно только поместить идентификатор выбранного элемента в панельNode.

selAreaStart(e) {
      // 框选节点开始 在mousedown的时候调用
      this.currentEvent = "sel_area_ing";
      const x =
        (e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) /
        this.svgScale;
      const y =
        (e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) /
        this.svgScale;
      this.simulate_sel_area = {
        left: x,
        top: y,
        width: 0,
        height: 0
      };
    },
    setSelAreaPostion(e) {
      // 框选节点ing  
      const x =
        (e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) /
        this.svgScale;
      const y =
        (e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) /
        this.svgScale;
      const width = x - this.simulate_sel_area.left;
      const height = y - this.simulate_sel_area.top;
      this.simulate_sel_area.width = width;
      this.simulate_sel_area.height = height;
    },
    getSelNodes(postions) {
      // 选取框选的节点
      const { left, top, width, height } = postions;
      this.choice.paneNode.length = 0;
      this.DataAll.nodes.forEach(item => {
        if (
          item.pos_x > left &&
          item.pos_x < left + width &&
          item.pos_y > top &&
          item.pos_y < top + height
        ) {
          this.choice.paneNode.push(item.id);
        }
      });
      console.log("目前选择的节点是", this.choice.paneNode);
    },

this.simulate_sel_area Поместите координаты начальной точки, высоту и ширину модального окна выбора кадра и передайте их компоненту для использования.

13. Организация мероприятия

До сих пор наш проект заполнен большим количеством событий, здесь мы можем управлять поведением события через currentEvent, запускать соответствующее событие путем прослушивания и распространять событие.

    /**
     * 事件分发器
     */
    dragIng(e) {
     // 事件发放器 根据currentEvent来执行系列事件
      switch (this.currentEvent) {
        case 'dragPane':
          if (e.timeStamp - this.timeStamp > 200) {
            this.currentEvent = "PaneDraging"; // 确认是拖动节点
          };
          break;
        case 'PaneDraging':
           this.setDragFramePosition(e); // 触发节点拖动
           break;
        case 'dragLink':
          this.setDragLinkPostion(e); // 触发连线拖动
          break;
        case 'sel_area_ing':
          this.setSelAreaPostion(e); // 触发框选
          break;
        case 'move_graph':
          this.graphMoveIng(e);
          break;
        default: () => { }
      }
    }

Просмотрев весь контент, в общей сложности на реализацию требований к визуализации модели и разделение компонентов ушло три недели. Я надеюсь оказать простую помощь нуждающимся коллегам. Все коды не являются лучшими практиками, и я просто хочу бросать кирпичи и привлекать нефрита.

Конкретный код можно посмотреть на githubНажмите, чтобы прыгнуть: https://github.com/murongqimiao/DAGBoard.

или перейти кzhanglizhong.cnПосмотреть ДЕМО