vue+draggable +jsPlumb 表格数据连线

华瀚漠
2023-12-01

 业务逻辑,类似与navcat中的视图可视化的操作 拖动列表 生成表格并支持连线

展示效果

​​​​

    <el-row>
      <el-col :span="4">
        <draggable
          v-model="tableData"
          @end="onEnd"
          :group="{ name: 'test', pull: 'clone', put: 'false' }"
          :sort="false"
          @start="startDra"
        >
          <div v-for="(element, index) in tableData" :key="index">
            {{ element.name }}
          </div>
        </draggable></el-col
      >
      <el-col :span="12">
        <draggable
          draggable
          style="height: 100vh; position: relative"
          v-model="tableData2"
          :group="{ name: 'test', pull: '' }"
          @add="addList"
          id="container"
        >
          <div v-for="(element, index) in tableData2" :key="index">
            <div
              class="chatBox fx-d-c"
              v-drag
              :id="element.name + '_' + element.node"
            >
              <div class="topBar fx-a-c">
                <div class="flex1 fx-a-c drayView" style="width: 100%">
                  <span class="chatName">{{ element.name }}</span>
                </div>
                <i
                  class="el-icon-edit"
                  style="
                    margin-right: 10px;
                    height: 55px;
                    width: 30px;
                    line-height: 55px;
                    cursor: pointer;
                  "
                  @click="closeChatBox($event)"
                ></i>
              </div>
              <ul>
                <li
                  v-for="(item, index) in element.list"
                  :key="'left' + index"
                  :id="item.id"
                  name="source"
                  class="lines"
                >
                  {{ item.name }}
                </li>
              </ul>
            </div>
          </div>
        </draggable></el-col
      >
    </el-row>

vue 自定义的可拖拽组件

import Vue from 'vue';
Vue.directive('drag', function (el, binding, vnode, oldVnode) {
  const dialogHeaderEl = el.querySelector('.drayView');
  // 监听 当前绑定指令元素的鼠标点下去的事件
  dialogHeaderEl.onmousedown = function (e) {
    const { ox, oy } = {
      ox: e.clientX - el.offsetLeft,
      oy: e.clientY - el.offsetTop,
    };
    let fit = false;
    let clear = true;
    // 监听鼠标来回移动的事件
    document.onmousemove = function (em) {
      const { left, top } = {
        left: em.clientX - ox,
        top: em.clientY - oy,
      };
      if ((left != 0 || top != 0) && clear) {
        vnode.context.clearLine();
        clear = false;
      }
      // ;
      // 将鼠标 换成小手
      el.style.cursor = 'pointer';
      // 和盒子的定位 改变left 和 top的值
      el.style.left = left + 'px';
      el.style.top = top + 'px';
    };
    // 监听鼠标抬起的事件
    document.onmouseup = function (eu) {
      document.onmousemove = null;
      if (!fit) {
        fit = true;
        vnode.context.ConnectLine();
      }
      el.style.cursor = 'default';
    };
  };
});

数据源的准备工作

 tableData: [
        {
          date: '2016-05-02',
          name: '王涛',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
          node: '1',
          tag: '家',
          list: [
            { id: 1, name: 123 },
            { id: 7, name: 'wt' },
          ],
        },
        {
          date: '2016-05-04',
          name: '王涛2',
          province: '上海',
          node: '2',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1517 弄',
          zip: 200333,
          list: [{ id: 2, name: 23 }],
          tag: '公司',
        },
        {
          date: '2016-05-01',
          name: '王涛3',
          province: '上海',
          node: '3',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1519 弄',
          zip: 200333,
          tag: '家',
          list: [{ id: 3, name: 1023 }],
        },
        {
          date: '2016-05-03',
          name: '王涛4',
          province: '上海',
          node: '4',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1516 弄',
          zip: 200333,
          tag: '公司',
          list: [{ id: 4, name: 1034 }],
        },
      ],
      readyList: [],
      tableData2: [
        {
          date: '2016-05-02',
          name: '王涛',
          province: '上海',
          node: '10',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
          list: [
            { id: 5, name: '男' },
            { id: 6, name: '我是老六' },
          ],
          tag: '家',
        },
      ],

初始化方法以及一些连线的方法

    showPlumb() {
      this.jsPlumb = this.$jsPlumb.getInstance({
        Container: 'container', // 选择器id
        EndpointStyle: { radius: 0.11, fill: '#999' }, // 端点样式
        PaintStyle: { stroke: '#999', strokeWidth: 2 }, // 绘画样式,默认8px线宽  #456
        HoverPaintStyle: { stroke: '#994B0A', strokeWidth: 3 }, // 默认悬停样式  默认为null
        ConnectionOverlays: [
          // 此处可以设置所有箭头的样式
          [
            'Arrow',
            {
              // 设置参数可以参考中文文档
              location: 1,
              length: 12,
              paintStyle: {
                stroke: '#999',
                fill: '#999',
              },
            },
          ],
        ],
        Connector: ['Straight'], // 要使用的默认连接器的类型:直线,折线,曲线等
        DrapOptions: { cursor: 'crosshair', zIndex: 2000 },
      });
      this.jsPlumb.batch(() => {
        this.tableData2.forEach((element, index) => {
          element.list.forEach((list, i) => {
            this.initLeaf('ids' + this.tableData2[index].list[i].id, 'source');
            this.initLeaf('ids' + this.tableData2[index].list[i].id, 'target');
          });
        });
      });
      this.setjsPlumb(true, true);
      this.ConnectLine();
      //点击连线
      this.jsPlumb.bind('click', (conn, originalEvent) => {
        this.LinkData.forEach((element, index) => {
          if (
            element.sourceId == conn.sourceId &&
            element.targetId == conn.targetId
          ) {
            this.LinkData.splice(index, 1);
          }
        });
        this.jsPlumb.deleteConnection(conn);
      });
      //连线时触发
      this.jsPlumb.bind('connection', (conn, originalEvent) => {
        let obj = {
          sourceId: conn.sourceId,
          targetId: conn.targetId,
        };
        this.LinkData.push(obj);
        this.readyList = JSON.parse(JSON.stringify(this.LinkData));
        this.LinkData = this.readyList.reduce((curr, next) => {
          /*判断对象中是否已经有该属性  没有的话 push 到 curr数组*/
          obj[next.sourceId + next.targetId]
            ? ''
            : (obj[next.sourceId + next.targetId] = curr.push(next));
          return curr;
        }, []);
      });

      //右键触发
      this.jsPlumb.bind('contextmenu', (conn, originalEvent) => {});
    },
    startDra() {
      this.jsPlumb.deleteEveryConnection();
      this.jsPlumb.deleteEveryEndpoint();
    },
    initLeaf(id, type) {
      const ins = this.jsPlumb;
      const elem = document.getElementById(id);
      if (type === 'source') {
        ins.makeSource(elem, {
          anchor: [1, 0.5, 0, 0], // 左 上 右 下
          allowLoopback: false, //允许回连
          maxConnections: -1, //最大连接数(-1表示不限制)
        });
      } else {
        ins.makeTarget(elem, {
          anchor: [0, 0.5, 0, 0],
          allowLoopback: false,
          maxConnections: -1,
        });
      }
    },
    //初始化连接线
    ConnectLine() {
      let allArr = [];
      for (var i = 0; i < this.LinkData.length; i++) {
        var flag = true;
        for (var j = 0; j < allArr.length; j++) {
          if (
            this.LinkData[i].targetId == allArr[j].targetId &&
            this.LinkData[i].sourceId == allArr[j].sourceId
          ) {
            flag = false;
          }
        }
        if (flag) {
          allArr.push(this.LinkData[i]);
        }
      }
      setTimeout(() => {
        for (const item of allArr) {
          this.jsPlumb.connect({
            source: item.sourceId,
            target: item.targetId,
          });
        }
      }, 0);
    },

完整代码

<template>
  <div>
    <el-row>
      <el-col :span="4">
        <draggable
          v-model="tableData"
          @end="onEnd"
          :group="{ name: 'test', pull: 'clone', put: 'false' }"
          :sort="false"
          @start="startDra"
        >
          <div v-for="(element, index) in tableData" :key="index">
            {{ element.name }}
          </div>
        </draggable></el-col
      >
      <el-col :span="12">
        <draggable
          draggable
          style="height: 100vh; position: relative"
          v-model="tableData2"
          :group="{ name: 'test', pull: '' }"
          @add="addList"
          id="container"
        >
          <div v-for="(element, index) in tableData2" :key="index">
            <div class="chatBox fx-d-c" v-drag :id="'element_' + element.node">
              <div class="topBar fx-a-c">
                <div class="flex1 fx-a-c drayView" style="width: 100%">
                  <span class="chatName">{{ element.name }}</span>
                </div>
                <i
                  class="el-icon-edit"
                  style="
                    margin-right: 10px;
                    height: 55px;
                    width: 30px;
                    line-height: 55px;
                    cursor: pointer;
                  "
                  @click="closeChatBox($event, element)"
                ></i>
              </div>
              <ul>
                <li
                  v-for="(item, index) in element.list"
                  :key="'left' + index"
                  :id="'ids' + item.id"
                  name="source"
                  class="lines"
                >
                  {{ item.name }}
                </li>
              </ul>
            </div>
          </div>
        </draggable></el-col
      >
    </el-row>
  </div>
</template>

<script>
import Vue from 'vue';
Vue.directive('drag', function (el, binding, vnode, oldVnode) {
  const dialogHeaderEl = el.querySelector('.drayView');
  // 监听 当前绑定指令元素的鼠标点下去的事件
  dialogHeaderEl.onmousedown = function (e) {
    const { ox, oy } = {
      ox: e.clientX - el.offsetLeft,
      oy: e.clientY - el.offsetTop,
    };
    let fit = false;
    let clear = true;
    // 监听鼠标来回移动的事件
    document.onmousemove = function (em) {
      const { left, top } = {
        left: em.clientX - ox,
        top: em.clientY - oy,
      };
      if ((left != 0 || top != 0) && clear) {
        vnode.context.clearLine();
        clear = false;
      }
      // ;
      // 将鼠标 换成小手
      el.style.cursor = 'pointer';
      // 和盒子的定位 改变left 和 top的值
      el.style.left = left + 'px';
      el.style.top = top + 'px';
    };
    // 监听鼠标抬起的事件
    document.onmouseup = function (eu) {
      document.onmousemove = null;
      if (!fit) {
        fit = true;
        vnode.context.ConnectLine();
      }
      el.style.cursor = 'default';
    };
  };
});
export default {
  data() {
    return {
      jsPlumb: null,
      //初始的id
      LinkData: [],
      tableData: [
        {
          date: '2016-05-02',
          name: '王涛',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
          node: '1',
          tag: '家',
          list: [
            { id: 1, name: 123 },
            { id: 7, name: 'wt' },
          ],
        },
        {
          date: '2016-05-04',
          name: '王涛2',
          province: '上海',
          node: '2',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1517 弄',
          zip: 200333,
          list: [{ id: 2, name: 23 }],
          tag: '公司',
        },
        {
          date: '2016-05-01',
          name: '王涛3',
          province: '上海',
          node: '3',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1519 弄',
          zip: 200333,
          tag: '家',
          list: [{ id: 3, name: 1023 }],
        },
        {
          date: '2016-05-03',
          name: '王涛4',
          province: '上海',
          node: '4',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1516 弄',
          zip: 200333,
          tag: '公司',
          list: [{ id: 4, name: 1034 }],
        },
      ],
      readyList: [],
      tableData2: [
        {
          date: '2016-05-02',
          name: '王涛',
          province: '上海',
          node: '10',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
          list: [
            { id: 5, name: '男' },
            { id: 6, name: '我是老六' },
          ],
          tag: '家',
        },
      ],
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.showPlumb();
    });
  },
  methods: {
    showPlumb() {
      this.jsPlumb = this.$jsPlumb.getInstance({
        Container: 'container', // 选择器id
        EndpointStyle: { radius: 0.11, fill: '#999' }, // 端点样式
        PaintStyle: { stroke: '#999', strokeWidth: 2 }, // 绘画样式,默认8px线宽  #456
        HoverPaintStyle: { stroke: '#994B0A', strokeWidth: 3 }, // 默认悬停样式  默认为null
        ConnectionOverlays: [
          // 此处可以设置所有箭头的样式
          [
            'Arrow',
            {
              // 设置参数可以参考中文文档
              location: 1,
              length: 12,
              paintStyle: {
                stroke: '#999',
                fill: '#999',
              },
            },
          ],
        ],
        Connector: ['Straight'], // 要使用的默认连接器的类型:直线,折线,曲线等
        DrapOptions: { cursor: 'crosshair', zIndex: 2000 },
      });
      this.jsPlumb.batch(() => {
        this.tableData2.forEach((element, index) => {
          element.list.forEach((list, i) => {
            this.initLeaf('ids' + this.tableData2[index].list[i].id, 'source');
            this.initLeaf('ids' + this.tableData2[index].list[i].id, 'target');
          });
        });
      });
      this.setjsPlumb(true, true);
      this.ConnectLine();
      //点击连线
      this.jsPlumb.bind('click', (conn, originalEvent) => {
        this.LinkData.forEach((element, index) => {
          if (
            element.sourceId == conn.sourceId &&
            element.targetId == conn.targetId
          ) {
            this.LinkData.splice(index, 1);
          }
        });
        this.jsPlumb.deleteConnection(conn);
      });
      //连线时触发
      this.jsPlumb.bind('connection', (conn, originalEvent) => {
        let obj = {
          sourceId: conn.sourceId,
          targetId: conn.targetId,
        };
        this.LinkData.push(obj);
        this.readyList = JSON.parse(JSON.stringify(this.LinkData));
        this.LinkData = this.readyList.reduce((curr, next) => {
          /*判断对象中是否已经有该属性  没有的话 push 到 curr数组*/
          obj[next.sourceId + next.targetId]
            ? ''
            : (obj[next.sourceId + next.targetId] = curr.push(next));
          return curr;
        }, []);
      });

      //右键触发
      this.jsPlumb.bind('contextmenu', (conn, originalEvent) => {});
    },
    startDra() {
      this.jsPlumb.deleteEveryConnection();
      this.jsPlumb.deleteEveryEndpoint();
    },
    initLeaf(id, type) {
      const ins = this.jsPlumb;
      const elem = document.getElementById(id);
      if (type === 'source') {
        ins.makeSource(elem, {
          anchor: [1, 0.5, 0, 0], // 左 上 右 下
          allowLoopback: false, //允许回连
          maxConnections: -1, //最大连接数(-1表示不限制)
        });
      } else {
        ins.makeTarget(elem, {
          anchor: [0, 0.5, 0, 0],
          allowLoopback: false,
          maxConnections: -1,
        });
      }
    },
    //初始化连接线
    ConnectLine() {
      let allArr = [];
      for (var i = 0; i < this.LinkData.length; i++) {
        var flag = true;
        for (var j = 0; j < allArr.length; j++) {
          if (
            this.LinkData[i].targetId == allArr[j].targetId &&
            this.LinkData[i].sourceId == allArr[j].sourceId
          ) {
            flag = false;
          }
        }
        if (flag) {
          allArr.push(this.LinkData[i]);
        }
      }
      setTimeout(() => {
        for (const item of allArr) {
          this.jsPlumb.connect({
            source: item.sourceId,
            target: item.targetId,
          });
        }
      }, 0);
    },
    clearLine() {
      this.jsPlumb.deleteEveryConnection();
      this.jsPlumb.deleteEveryEndpoint();
      this.jsPlumb.setSuspendDrawing(false, true);
    },
    setjsPlumb(sourceFlag, targetFlag) {
      const source = document.getElementsByName('source');
      const target = document.getElementsByName('target');
      this.jsPlumb.setSourceEnabled(source, sourceFlag);
      this.jsPlumb.setTargetEnabled(target, targetFlag);
      this.jsPlumb.setDraggable(source, false); // 是否支持拖拽
      this.jsPlumb.setDraggable(target, false); // 是否支持拖拽
    },
    closeChatBox(e, row) {
      let node = e.currentTarget.parentNode.parentNode;
      let b = e.currentTarget.parentNode.parentNode.getElementsByTagName('li');
      //深拷贝防止数组变化
      const allLines = [...this.jsPlumb.getConnections()];
      const LinkData = [...this.LinkData];
      for (const iterator of b) {
        allLines.forEach((element, index) => {
          if (
            iterator.id == element.sourceId ||
            iterator.id == element.targetId
          )
            this.jsPlumb.deleteConnection(element);
        });
        this.LinkData = LinkData.filter(item => {
          return iterator.id !== item.sourceId && iterator.id !== item.targetId;
        });
      }
      node.remove();
      row.isDel = 1;
      this.$forceUpdate();
      this.ConnectLine();
    },
    onEnd(e) {
      // console.log(e, '11111111');
    },
    addList(e) {
      e.item._underlying_vm_.list.forEach((list, i) => {
        this.initLeaf('ids' + list.id, 'source');
        this.initLeaf('ids' + list.id, 'target');
      });
      this.ConnectLine();
    },
  },
};
</script>
<style lang="less" scoped>
.chatBox {
  //不能用fixed 不然会出现错乱
  position: absolute;
  width: 150px;
  height: 300px;
  right: 130px;
  top: 384px;
  // z-index: 1;
  background: white;
  box-shadow: 0px 1px 6px 0px rgba(0, 21, 41, 0.12);
  .topBar {
    width: 150px;
    height: 56px;
    display: flex;
    line-height: 56px;
    box-shadow: 0px 1px 6px 0px rgba(0, 21, 41, 0.12);
    z-index: 10;
    cursor: move;
  }
  .chatAvatar {
    width: 36px;
    height: 36px;
    overflow: hidden;
    margin-left: 10px;
    object-fit: cover;
  }
  .chatName {
    width: 50px;
    margin-left: 12px;
    font-size: 16px;
    font-weight: 600;
    color: #333333;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .chatContent {
    padding: 8px 0;
    overflow-y: auto;
    overflow-x: hidden;
  }
}
.flex1 {
  display: flex;
}
.el-icon-edit {
  z-index: 100;
}
</style>

 类似资料: