vue结合g6实现树级结构(compactBox 紧凑树)

 更新时间:2023年06月20日 10:19:09   作者:我有一棵向日葵  
本文主要介绍了vue结合g6实现树级结构,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

最近公司项目需求有用到树级结构,所以就上网上找了点资料,本文就实现vue结合g6实现树级结构,具体如下:

G6文档

自定义节点

G6.registerNode(
  "dom-node",
  {
    draw: (cfg, group) => {
      let str = `
        <div class='item-box catalog-node ${
          cfg.isSelected ? "is-selected" : ""
        } ${cfg.status}-box' οnclick='handleDetail("${cfg.id}")' id="${
        cfg.id
      }" style="width: ${cfg.size[0] - 5}px;">
          ${
            cfg.status
              ? `<span class='status ${cfg.status}'>${getLabel(
                  ISSUE_STATUS,
                  cfg.status
                )}</span>`
              : ""
          }
          ${
            cfg?.manager?.name
              ? `<p class=''><span class="title-txt avatar-img" title='负责人'>
            <img 
              src="${cfg?.manager?.avatar}"
            />
            </span>${cfg.manager.name}
            </p>`
              : ""
          }
          <div class='title' οnclick='handleDetail("${cfg.id}")'><span ${
        cfg.typeName === "Bug" ? `class='tipText'` : ""
      }>${cfg.title}</span>
          </div>
        </div>
        `;
      return group.addShape("dom", {
        attrs: {
          width: cfg.size[0],
          height: nodeHeight(cfg),
          // 传入 DOM 的 html
          html: str,
        },
        draggable: true,
      });
    },
  },
  "single-node"
);

在pc端,自定义的节点,绑定的点击事件起作用,但是移动端模式,不会起作用;

解决方法:

文档中也说明了,节点的选中事件,需要将Mode切换到edit模式。graph.setMode("edit");(模式可自定义)

如:

graph.setMode("edit");
graph.on("nodeselectchange", (e) => {
  // 当前操作的 item
  alert("node");
  console.log(e.target);
  // 当前操作后,所有被选中的 items 集合
  console.log(e.selectedItems);
  // 当前操作时选中(true)还是取消选中(false)
  console.log(e.select);
});

此处,直接用自定节点的事件了。

以上只是解决了移动端不能触发点击事件,但是移动端存在拖动canvas与点击事件冲突问题,所以为了PC端与移动端都能起效果,做了以下处理。

  • 设置两个模式,default: [“drag-node”, “drag-canvas”, “click-select”]; edit: [‘click-select’]。edit 模式还需要添加个自定义的Behavior(G6.registerBehavior(“behavior-name”, {})),自定义鼠标拖动事件;
  • 所以,先判断是否是移动端,如果是移动端,setMode(‘edit’);

全部代码

<template>
  <div id="container"></div>
</template>
<script>
// 引入antv-G6
import G6 from "@antv/g6";
import { ISSUE_STATUS } from "@/utils/constant";
import { getLabel } from "@/utils";
// G6的配置项
G6.registerNode(
  "icon-node",
  {
    options: {
      size: [60, 20], // 宽高
      stroke: "#91d5ff", // 变颜色
      fill: "#fff", // 填充色
    },
    // draw是绘制后的附加操作-节点的配置项  图形分组,节点中图形对象的容器
    draw(cfg, group) {
      // 获取节点的配置
      const styles = this.getShapeStyle(cfg);
      // 解构赋值
      const { labelCfg = {} } = cfg;
      const w = styles.width;
      const h = styles.height;
      // 向分组中添加新的图形 图形 配置 rect矩形 xy 代表左上角坐标 w h是宽高
      const keyShape = group.addShape("rect", {
        attrs: {
          ...styles,
          x: -w / 2,
          y: -h / 2,
        },
      });
      // 文本文字的配置
      if (cfg.title) {
        group.addShape("text", {
          attrs: {
            ...labelCfg.style,
            text: cfg.title,
            x: 50 - w / 2,
            y: 25 - h / 2,
          },
        });
      }
      return keyShape;
    },
    // 更新节点后的操作,一般同 afterDraw 配合使用
    update: undefined,
  },
  "rect"
);
const nodeHeight = (obj) => {
  // if (obj.depth == 0) {
  //   return 100;
  // }
  const l = ["manager", "title"];
  const arr = l.filter((item) => {
    return obj[item];
  });
  return arr.length * 25 + 50;
};
G6.registerNode(
  "dom-node",
  {
    draw: (cfg, group) => {
      let str = `
        <div class='item-box catalog-node ${
          cfg.isSelected ? "is-selected" : ""
        } ${cfg.status}-box' οnclick='handleDetail("${cfg.id}")' id="${
        cfg.id
      }" style="width: ${cfg.size[0] - 5}px;">
          ${
            cfg.status
              ? `<span class='status ${cfg.status}'>${getLabel( ISSUE_STATUS, cfg.status )}</span>`
              : ""
          }
          ${
            cfg?.manager?.name
              ? `<p class=''><span class="title-txt avatar-img" title='负责人'> <img src="${cfg?.manager?.avatar}" /> </span>${cfg.manager.name} </p>`
              : ""
          }
          <div class='title' οnclick='handleDetail("${cfg.id}")'><span ${
        cfg.typeName === "Bug" ? `class='tipText'` : ""
      }>${cfg.title}</span>
          </div>
        </div>
        `;
      return group.addShape("dom", {
        attrs: {
          width: cfg.size[0],
          height: nodeHeight(cfg),
          // 传入 DOM 的 html
          html: str,
        },
        draggable: true,
      });
    },
  },
  "single-node"
);
// 绘制层级之间的连接线
G6.registerEdge("flow-line", {
  // 绘制后的附加操作
  draw(cfg, group) {
    // 边两端与起始节点和结束节点的交点;
    const startPoint = cfg.startPoint;
    const endPoint = cfg.endPoint;
    // 边的配置
    const { style } = cfg;
    const shape = group.addShape("path", {
      attrs: {
        stroke: style.stroke, // 边框的样式
        endArrow: style.endArrow, // 结束箭头
        // 路径
        path: [
          ["M", startPoint.x, startPoint.y],
          ["L", startPoint.x, (startPoint.y + endPoint.y) / 2],
          ["L", endPoint.x, (startPoint.y + endPoint.y) / 2],
          ["L", endPoint.x, endPoint.y],
        ],
      },
    });
    return shape;
  },
});
// 默认连接边线的颜色 末尾箭头
const defaultEdgeStyle = {
  stroke: "#ccc",
};
// 默认布局
// compactBox 紧凑树布局
// 从根节点开始,同一深度的节点在同一层,并且布局时会将节点大小考虑进去。
const defaultLayout = {
  type: "compactBox", // 布局类型树
  direction: "TB", // TB 根节点在上,往下布局
  getId: function getId(d) {
    // 节点 id 的回调函数
    return d.id;
  },
  getHeight: function getHeight() {
    // 节点高度的回调函数
    return 16;
  },
  getWidth: function getWidth() {
    // 节点宽度的回调函数
    return 16;
  },
  getVGap: function getVGap(d) {
    // 节点纵向间距的回调函数
    if (d.parId === "0") return 70;
    return 80;
  },
  getHGap: function getHGap(d) {
    // 节点横向间距的回调函数
    if (d.parId === "0") return 100;
    return 150;
  },
};
// 自定义拖动事件
G6.registerBehavior("finger-drag-canvas", {
  dragging: false,
  offset: 0,
  getEvents() {
    return {
      touchstart: "onDragStart",
      touchmove: "onDrag",
      touchend: "onDragEnd",
    };
  },
  onDragStart(e) {
    const self = this;
    self.dragging = false;
    self.offset = 0;
    const clientX = +e.clientX;
    const clientY = +e.clientY;
    this.origin = {
      x: clientX,
      y: clientY,
    };
  },
  onDrag(e) {
    const { graph } = this;
    if (!this.dragging) {
      this.dragging = true;
    }
    this.updateViewport(e);
  },
  onDragEnd(evt) {
    const edges = graph.getEdges();
    const nodes = graph.getNodes();
    const node = evt.item;
    const point = { x: evt.x, y: evt.y };
    // this.updateViewport(e);
    this.dragging = false;
    // 这里开始识别点击事件
    if (this.offset < 30) {
      // 触发点击事件(或者依靠e.target,e.type去做相应的业务操作)
      console.log(evt, evt.type);
    }
    console.log(evt, evt.type);
    this.updateViewport(evt);
  },
  updateViewport(e) {
    const { origin } = this;
    const clientX = +e.clientX;
    const clientY = +e.clientY;
    if (isNaN(clientX) || isNaN(clientY)) {
      return;
    }
    let dx = clientX - origin.x;
    let dy = clientY - origin.y;
    if (this.get("direction") === "x") {
      dy = 0;
    } else if (this.get("direction") === "y") {
      dx = 0;
    }
    this.origin = {
      x: clientX,
      y: clientY,
    };
    const width = graph.get("width");
    const height = graph.get("height");
    const graphCanvasBBox = graph.get("canvas").getCanvasBBox();
    if (
      (graphCanvasBBox.minX <= width && graphCanvasBBox.minX + dx > width) ||
      (graphCanvasBBox.maxX >= 0 && graphCanvasBBox.maxX + dx < 0)
    ) {
      dx = 0;
    }
    if (
      (graphCanvasBBox.minY <= height && graphCanvasBBox.minY + dy > height) ||
      (graphCanvasBBox.maxY >= 0 && graphCanvasBBox.maxY + dy < 0)
    ) {
      dy = 0;
    }
    if (dx === 0 && dy === 0) return;
    // 增加拖动距离统计
    this.offset += Math.abs(dx) + Math.abs(dy);
    graph.translate(dx, dy);
  },
});
let graph;
export default {
  name: "Home",
  props: {
    treeListData: {
      type: Array,
      default: () => [],
    },
    options: {
      type: Object,
      default: () => {
        return {};
      },
    },
  },
  emits: ["handleSelected"],
  data() {
    return {
      listData: [],
      selectedId: "", // 选中的节点Id
      initOptions: {
        isFitView: true, // 是否默认适应全局
        isFitCenter: true, // 是否居中
        isHiddenRoot: true, // 是否显示根元素
      },
      flag: false, // 如果是移动端,true
    };
  },
  methods: {
    G6init() {
      if (typeof window !== "undefined") {
        window.onresize = () => {
          if (!graph || graph.get("destroyed")) return;
          if (!container || !container.scrollWidth || !container.scrollHeight)
            return;
          graph.changeSize(container.scrollWidth, container.scrollHeight);
        };
      }
      // 获取容器
      const container = document.getElementById("container");
      // 获取容器的宽高
      const width = container.scrollWidth;
      const height = container.scrollHeight - 30 || 500;
      // Graph 是 G6 图表的载体-实例化
      graph = new G6.TreeGraph({
        container: "container", // 图的 DOM 容器
        width,
        height,
        linkCenter: true, // 指定边是否连入节点的中心
        modes: {
          // default 模式中包含点击选中节点行为和拖拽画布行为;
          default: [
            {
              type: "zoom-canvas",
              enableOptimize: true, //开启性能优化
            },
            "drag-node",
            "drag-canvas",
            // "zoom-canvas",
            "click-select",
          ],
          edit: ["click-select"],
        },
        // 默认状态下节点的配置
        defaultNode: {
          type: "dom-node", // 'icon-node',
          size: [250, 60],
        },
        // 默认状态下边线的配置,
        defaultEdge: {
          type: "flow-line",
          style: defaultEdgeStyle,
        },
        // 布局配置项
        layout: defaultLayout,
        renderer: "svg",
      });
      graph.data([...this.listData][0]);
      graph.render();
      // 让画布内容适应视口。
      if (this.initOptions.isFitView) {
        graph.fitView();
      }
      if (this.initOptions.isFitCenter) {
        graph.fitCenter();
      }
      if (!this.initOptions.isHiddenRoot) {
        // 是否要移除根节点
        const item = graph.findById([...this.listData][0].id);
        graph.removeItem(item);
      }
      // 改变视口的缩放比例,在当前画布比例下缩放,是相对比例。
      graph.zoom(1);
    },
    async init() {
      let _this = this;
      if (graph) {
        // 如果原来有画布,需要先清除
        graph.destroy();
      }
      this.initOptions = Object.assign(this.initOptions, this.options);
      this.listData = [...this.treeListData];
      function setSelectFalse(obj) {
        obj.forEach((element) => {
          element.isSelected = false;
          if (element.children) {
            setSelectFalse(element.children);
          }
        });
      }
      window.handleDetail = (id) => {
        const item = graph.findById(id);
        if (item?._cfg?.parent) {
          _this.$emit("handleSelected", id);
        }
      };
      this.G6init();
      this.isMobile();
    },
    isMobile() { // 判断是否是移动端
      this.flag = navigator.userAgent.match(
        /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
      );
      if (this.flag) {
        graph.setMode("edit");
        graph.addBehaviors("finger-drag-canvas", "edit");
      }
    },
  },
  beforeDestroy() {
    console.log("推出");
  },
};
</script>
<style lang="scss" scoped>
@import "@/assets/styles/common.scss";
#container {
  height: 100%;
  width: 100%;
  border: 1px solid #efefef;
  ::v-deep .title {
    font-size: 15px;
    display: block;
    // text-align: center;
    position: relative;
    margin: 10px 0;
    padding-left: 15px;
    color: #1199ff;
    cursor: pointer;
  }
  ::v-deep .item-box {
    background-color: #fff;
    border-radius: 5px;
    padding: 5px;
    // height: 100%;
    border: 1px solid;
    position: relative;
    p {
      margin-bottom: 2px;
      display: flex;
      align-items: center;
      color: #333;
    }
    &.is-selected {
      border: 1px solid #1199ff;
    }
    .tipText {
      color: red;
    }
    .logs {
      height: 70px;
      overflow: hidden;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3;
    }
    .title-txt {
      display: inline-block;
      width: 80px;
      color: rgb(169, 169, 169);
    }
    .avatar-img {
      width: 35px;
      height: 35px;
      margin-right: 15px;
      img {
        width: 100%;
        height: 100%;
        border-radius: 100%;
      }
    }
    .status {
      position: absolute;
      right: 15px;
      top: 15px;
      border: 1px solid;
      padding: 0 5px;
      font-size: 12px;
      border-radius: 4px;
      // Wait 未开始、Doing进行中、Pause暂停、Verify待验证、Done已完成、Cancel已取消
    }
  }
}
::v-deep g g g:not(:first-child) foreignObject {
  font-size: 14px;
}
foreignObject {
  overflow: initial !important;
}
</style>

到此这篇关于vue结合g6实现树级结构(compactBox 紧凑树)的文章就介绍到这了,更多相关vue g6 树级结构 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue初始化项目时报错:Error:Cannot find module vue-template-compiler解决

    vue初始化项目时报错:Error:Cannot find module vue-temp

    这篇文章主要介绍了vue初始化项目时报错:Error:Cannot find module vue-template-compiler问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • vite的搭建与使用的详细步骤

    vite的搭建与使用的详细步骤

    本文主要介绍了vite的搭建与使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • vue使用localStorage保存登录信息 适用于移动端、PC端

    vue使用localStorage保存登录信息 适用于移动端、PC端

    这篇文章主要为大家详细介绍了vue使用localStorage保存登录信息 适用于移动端、PC端,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • Vue使用Echarts实现横向柱状图,并通过WebSocket即时通讯更新

    Vue使用Echarts实现横向柱状图,并通过WebSocket即时通讯更新

    这篇文章主要介绍了Vue使用Echarts实现横向柱状图,并通过WebSocket即时通讯更新方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • Element-plus使用中遇到的问题小结

    Element-plus使用中遇到的问题小结

    表格数据是websocket通信获取的数据,首次获取20条数据,以后新增订阅获取一条,新增一条则向上滑动显示最新数据,本文给大家介绍Element-plus使用中遇到的问题小结,感兴趣的朋友跟随小编一起看看吧
    2024-04-04
  • Vue 路由传参加密的示例代码

    Vue 路由传参加密的示例代码

    这篇文章主要介绍了Vue 路由传参加密,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12
  • 深入探索VueJS Scoped CSS 实现原理

    深入探索VueJS Scoped CSS 实现原理

    这篇文章主要介绍了深入探索VueJS Scoped CSS 实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • element-ui复杂table表格动态新增列、动态调整行以及列顺序详解

    element-ui复杂table表格动态新增列、动态调整行以及列顺序详解

    这篇文章主要给大家介绍了关于element-ui复杂table表格动态新增列、动态调整行以及列顺序的相关资料,文中通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-08-08
  • vue2.0/3.0中provide和inject的用法示例

    vue2.0/3.0中provide和inject的用法示例

    provide和inject是成对出现的,主要用于父组件向子孙组件传递数据,这篇文章主要给大家介绍了关于vue2.0/3.0中provide和inject用法的相关资料,需要的朋友可以参考下
    2021-09-09
  • Vue中实现视频播放的示例详解

    Vue中实现视频播放的示例详解

    这篇文章主要为大家学习介绍了基于Vue如何实现视频播放的功能,文中的示例代码讲解详细,具有一定的参考价值,需要的小伙伴可以了解一下
    2023-08-08

最新评论