使用D3js 实现进行任务分配效果

使用D3js 实现进行任务分配效果不会抓妖的小道士的故事有个从来抓不到鬼的小道士,他终于还俗了,在他婚礼的那一天,他的师兄弟没有一个到场,可是全城的鬼怪都聚集在了婚礼上空,想为这个总是心肠很软、碎碎念、很唠叨,最后娶了个小狐狸的这个家伙当一道劫雷,可是那天,晴空万里,什么都没有发生;言归正传首先看效果该效果使用的是D3js+vue2进行使用时注意事项 D3js版本不宜过高否则会报错出现无法移动现象使用”^5.16.0″版本 由于我使用了load-scss如果自己安装无法启动可以使用我的

大家好,欢迎来到IT知识分享网。

不会抓妖的小道士的故事

有个从来抓不到鬼的小道士,他终于还俗了,在他婚礼的那一天,他的师兄弟没有一个到场,
可是全城的鬼怪都聚集在了婚礼上空,想为这个总是心肠很软、碎碎念、很唠叨,
最后娶了个小狐狸的这个家伙当一道劫雷,可是那天,晴空万里,什么都没有发生;

言归正传 首先看效果

使用D3js 实现进行任务分配效果

该效果使用的是D3js+vue2进行

使用时 注意事项

  • D3js版本不宜过高 否则会报错 出现无法移动现象 使用”^5.16.0″版本

  • 由于我使用了load-scss 如果自己安装无法启动可以使用我的版本

  • 部分依赖 都放在 devDependencies模块下

    “devDependencies”: { “d3”: “^5.16.0”, “sass-loader”: “^7.1.0”, “node-sass”: “^4.12.0”, }

  • 使用D3js 实现进行任务分配效果

具体步骤就不在这进行一一讲述了,太多了,你们看来也烦,就直接上代码,运行即可,代码文件如下 代码中需要注意的事项都在TODO里面写着,请认直阅读该注释,如有疑问可留言,感谢各位大佬的阅读

<template>
  <div class="task-container">
    <div :id="id" class="tree-container">
      <svg class="d3-tree ">
        <!-- 任务树 -->
        <g class="container"></g>
        <!-- 关联任务树 -->
        <g class="relationContainer"></g>
      </svg>
    </div>
  </div>
</template>
<script>
import * as d3 from 'd3'
export default {
  name: 'Home',
  data() {
    return {
      isTaskBegin: true,//任务是否开始  是计划还是还是开始执行任务
      id:'',//卡片唯一ID
      taskDetail: {},//任务数据
      width: null,
      height: null,
      zoom: null,//鼠标点击移动卡片速度
      treeData: {},
      popper_arrow: 'right',//鼠标练级移动的方向
      duration: 750,
      root: null,
      rootRelation: null,//卡片之间的关系
      popoverData: {},//移除卡片时接受的数据
      stopDialog: false,//
      clientX: 0,
      clientY: 0,
      x: 0,
      y: 0,
      scale: 1,
      refresh: 0,//页面刷新卡片刷新
      timer: null,
      transformXY: null,
    }
  },
  computed: {
    treemap() {
      return d3.tree().size([this.height, this.width]).nodeSize([400, 300]);
    }
  },
  created() {
    this.id = this.uuid()
  },
  mounted() {
    this.initSvg();
  },
  methods: {
    /**
     * 初始化画布与获取任务数据
     */
    initSvg() {
      //获取任务数据
      this.findTaskTrack()
      //创建svg画布
      this.width = document.getElementById(this.id).clientWidth
      this.height = document.getElementById(this.id).clientHeight
      const svg = d3.select(this.$el).select('svg.d3-tree');
      const container = d3.select(this.$el).select('g.container');
      const relationContainer = d3.select(this.$el).select('g.relationContainer');
      const transform = d3.zoomIdentity.translate(this.width / 2, 220).scale(0.8);
      // init zoom behavior, which is both an object and function
      this.zoom = d3.zoom().scaleExtent([1 / 12, 2]).on('zoom', (e) => {
        this.transformXY = d3.event.transform;
        clearTimeout(this.timer);
        this.timer = setTimeout(() => {
          container.attr('transform', this.transformXY)
          relationContainer.attr('transform', this.transformXY)
        }, this.refresh)

      })
      svg.transition().duration(0).call(this.zoom.transform, transform)
      svg.call(this.zoom)
    },
    /**
     * 每个任务不同的ID
     */
    uuid() {
      function s4() {
        return Math.floor((1 + Math.random()) * 0x10000)
            .toString(16)
            .substring(1)
      }
      return (
          s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
      )
    },
    /**
     * @description 获取构造根节点
     */
    getRoot(subTaskList) {
      let root = d3.hierarchy(this.treeData, d => {
        return d[subTaskList]
      })
      root.x0 = this.width / 2
      root.y0 = 220
      return root
    },
    /**
     * 点击大卡片节点事件 这里点击卡片的圆圈处会触发另一个事件 clickCircle
     * @param d 节点数据 包含任务数据
     * @param  d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
     */
    clickCard(d) {
      // console.log(d);
    },
    /**
     * 点击圆圈进行操作
     * @param d 节点数据
     * @param  d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
     */
    clickCircle(d) {
      console.log(d);
    },
    /**
     * 点击分解按钮
     * @param d 节点数据
     * @param  d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
     */
    decompose(d) {
      var e = d3.event;
      if (e.stopPropagation) {
        e.stopPropagation();
      } else {
        window.event.returnValue == false;
      }
      // 这里做一些对分解任务进行处理,进行弹窗跳转页面什么的
    },
    /**
     * 鼠标移上卡片时触发的事件
     */
    addVisible(d) {
      if (this.stopDialog) return this.stopDialog = false;
      this.popoverData = d.data;
      if (this.x + (d.x + 150) * this.scale > this.width - 320) {
        this.popper_arrow = 'left';
        this.clientX = this.x + (d.x - 150) * this.scale - 336;
      } else {
        this.popper_arrow = 'right';
        this.clientX = 16 + this.x + (d.x + 150) * this.scale;
      }
      this.clientY = this.y + (d.y - 190) * this.scale;
    },
    /**
     * 鼠标移开卡片的事件
     */
    delVisible(d) {
    },
    /**
     *
     */
    diagonal(s, d, g_className) {
      return g_className === 'container' ? `M ${s.x} ${s.y}C ${s.x} ${d.y},${s.x} ${d.y},${d.x} ${d.y}` : `M ${s.x} ${-s.y - 210}C ${s.x} ${-d.y - 210},${s.x} ${-d.y - 210},${d.x} ${-d.y - 210}`
    },
    /**
     * @description 数据与Dom进行绑定
     */
    update(source, g_className) {
      let dTreeData = null;
      dTreeData=g_className === 'container'?this.treemap(this.root):this.treemap(this.rootRelation);
      let nodes = dTreeData.descendants()
      let links = dTreeData.descendants().slice(1)
      nodes.forEach(d => {
        d.y = d.depth * 280
      })
      const svg = d3.select(this.$el).select('svg.d3-tree')
      const container = svg.select(`g.${g_className}`)
      let node = container.selectAll('g.node').data(nodes, d => {
        return d.id
      })
      let nodeEnter = node.enter().append('g').attr('class', 'node').attr('transform', d => {
        return 'translate(' + source.x0 + ',' + source.y0 + ')'
      })
      // 节点画图
      this.canvasNode(nodeEnter, g_className)
      // Transition nodes to their new position.
      let nodeUpdate = nodeEnter.merge(node).transition().duration(this.duration).attr("transform", function (d) {
        return g_className === 'container'?"translate(" + d.x + "," + d.y + ")":"translate(" + d.x + "," + -d.y + ")";
      });
      // 节点更新
      nodeUpdate.select("circle").style("fill", function (d) {
        if (d.children) return "url(#image_jian)";
        if (d._children) return "url(#image_jia)";
        return "#fff";
      });
      nodeUpdate.select("text")
          .style("fill-opacity", 1)
      let nodeExit = node.exit().transition().duration(this.duration).attr("transform", function (d) {
        return g_className === 'container'?"translate(" + source.x + "," + source.y + ")":"translate(" + source.x + "," + -source.y + ")";
      }).remove();
      let link = container.selectAll('path.link').data(links, d => {
        return d.id
      })
      //TODO taskId 为数据Id 如果不同可自行修改
      let linkEnter = link.enter().insert("path", "g").attr("class", "link").attr("id", (d) => {
        return d.data.taskId
      }).attr("d", d => {
        let o = {x: source.x0, y: source.y0};
        return this.diagonal(o, o, g_className)
      }).attr("fill", 'none').attr("stroke-width", 1).attr('stroke', '#ccc');
      let textpath = container.selectAll('text.textpath').data(links, d => {
        return d.id
      })
      let textLink = textpath.enter().insert("text", "path").attr("class", "textpath").attr("x", (d) => {
        if (Math.abs(d.parent.x - d.x) > 220) return 320
        return 220 //Math.abs(d.x/2)
      }).attr("y", (d) => {
        return g_className === 'container'?d.y:-d.y
      }).attr("text-anchor", 'top');
      //TODO taskId 为数据Id 如果不同可自行修改
      textLink.append('textPath').attr("fill", "#2986cf").attr("xlink:href", (d) => {
        return '#' + d.data.taskId
      }).attr('start-offset', "50%").style('font-size', 12).text(function (d) {
        //TODO 这里显示卡片与卡片之间的关系
        return g_className === 'container'?'分解':'关联'
      })
      let linkUpdate = linkEnter.merge(link)
      linkUpdate.transition().duration(this.duration).attr('d', d => {
        return this.diagonal(d, d.parent, g_className)
      })
      link.exit().transition().duration(this.duration).attr("d", d => {
        let o = {x: source.x, y: source.y};
        return this.diagonal(o, o, g_className)
      }).remove();
      let textUpdate = textLink.merge(textpath)
      textUpdate.transition().duration(this.duration).attr('d', d => {
        return this.diagonal(d, d.parent, g_className)
      })

      // Transition exiting nodes to the parent's new position.
      textpath.exit().transition().duration(this.duration).attr("d", d => {
        let o = {x: source.x, y: source.y};
        return this.diagonal(o, o, g_className)
      }).remove();
      // Stash the old positions for transition.
      nodes.forEach(d => {
        d.x0 = d.x
        d.y0 = d.y
      })
      // 渲染结束后, 转圈圈消失 TODO 这里可以设置数据加载的过程
      // this.loading = false;
    },
    // 画节点模块
    canvasNode(nodeEnter, g_className) {
      nodeEnter.append("circle").attr("r", 10).attr("stroke", "#fff").attr("stroke-width", 1)
          .on('click', (d) => {
            this.stopDialog = true;
            if (!d._children && !d.children)
              return
            if (d.children) {
              this.$set(d, '_children', d.children)
              d.children = null
            } else {
              this.$set(d, 'children', d._children)
              d._children = null
            }
            this.$nextTick(() => {
                  this.update(d, g_className)
                }
            )
          }).style("fill", function (d) {
        if (d.children) return "url(#image_jian)";
        if (d._children) return "url(#image_jia)";
        return "#fff";
      }).attr("transform", function (d) {
        return g_className === 'container'?"translate(" + 0 + "," + 10 + ")":"translate(" + 0 + "," + -220 + ")"
          }).style("display", (d) => {
        return d.children || d._children ? 'block' : 'none'
      })
      // TODO 创建一个 卡片
      let rectText = nodeEnter.append("g").on('click', this.clickCard).on('mouseenter', this.addVisible).on('mouseleave', this.delVisible)
          .style("fill-opacity", 1).style("cursor", (d) => {
            return this.isTaskBegin?'default':'pointer'
          }).attr("transform", function (d) {
            return "translate(" + 0 + "," + 0 + ")";
          });
      // 节点背景图 TODO 卡片样式
      rectText.append("rect")
          .attr("x", -150)
          .attr("y", -190)
          .attr("rx", 6)
          .attr("ry", 6)
          .attr("width", 300)
          .attr("height", 210)
          .attr("fill", "white")
          .attr("stroke", (d) => {
            // TODO 这里进行区分卡片等级颜色
            return d.data.taskLevel === 1?"#33a39c":"#999";
          }).style("fill-opacity", 1);
      //TODO 标题区域
      let titleText = rectText.append("g").attr("transform", function (d) {
        return "translate(" + -150 + "," + -210 + ")";
      });
      titleText.append("rect")
          .attr("width", 300)
          .attr("height", 40)
          .attr("rx", 6)
          .attr("ry", 6)
          .attr("fill", (d) => {
            if (d.data.taskLevel === 1) {
              return '#27B148'
            }
            return g_className === 'container'?"#33a39c":"#2986cf"
          }).attr("stroke", (d) => {
          if (d.data.taskLevel === 1) {
            return '#27B148'
          }
          return g_className === 'container'?"#33a39c":"#2986cf"
      }).style("fill-opacity", 1);
      //TODO 任务名字 对于位置 x y
      let titleName = titleText.append("svg")
          .attr("x", 0)
          .attr("y", 0)
          .attr("width", 300)
          .attr("height", 30)
          .style("fill-opacity", 1);
      titleName.append("title")
          .attr("dy", 20)
          .text(function (d) {
            return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName}`
          }).style("fill-opacity", 1);
      titleName.append("text").attr("x", (d) => {
        return d.data.taskName.length >= 9 && g_className === 'container' ? 130 : 150
      }).attr("y", 0).attr("dy", 26).attr("fill", "#fff").attr("text-anchor", 'middle').text(function (d) {
        if (d.data.taskName && d.data.taskName.length >= 9) {
          return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName.substring(0, 8)}...`
        } else {
          return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName}`
        }
      }).style("fill-opacity", 1);
      //TODO 分解按钮 任务分解按钮显示的位置 x表示在卡片x左边的位置 y >0 表示向下移动的位置
      titleText.append("text")
          .on('click', this.decompose)
          .attr("x", 250)
          .attr("y", 0)
          .attr("dy", 26)
          .attr("fill", "#0000dd")
          .attr("text-anchor", 'right')
          .text('(分解)')
          .style("display", (d) => {
            // TODO 进行显示隐藏分解按钮 如果数据中有一个字段,可以通过 d.data.XX属性进行判断是否显示
            return 1 ? 'block' : 'none'
          }).style("text-decoration", "underline").style("fill-opacity", 1);
      //TODO 审核人、创建人 这里值得注意 除了标题以外 下面添加的属性 x基本固定为-140 y轴为负值,从该卡片右下角为卡片坐标系原点 所以为负数
      let superAuditorName = rectText.append("svg")
          .attr("x", -140)
          .attr("y", -150)
          .attr("width", 200)
          .attr("height", 30)
          .style("fill-opacity", 1);
      superAuditorName.append("text").attr("dy", 20).attr("text-anchor", 'left').text((d) => {
        if (d.data.taskLevel === 1) {
          if (d.data.executorUserName && d.data.executorUserName.length >= 10) {
            return '创建人:' + d.data.executorUserName.substring(0, 8) + '...'
          } else {
            return '创建人:' + (d.data.executorUserName ? d.data.executorUserName : '无')
          }
        } else {
          if (d.data.auditorName && d.data.auditorName.length >= 10) {
            return '审核人:' + d.data.auditorName.substring(0, 8) + '...'
          } else {
            return '审核人:' + (d.data.auditorName ? d.data.auditorName : '无')
          }
        }
      }).style("fill-opacity", 1);
      // TODO 处理人 这里值得注意 如果有些字段你某些任务没有 你得对y坐标进行处理 虽然你可以使用 style("display","none") 进行隐藏 但最终这个位置就是一个空缺 不美观
      let executorUserName = rectText.append("svg")
          .attr("x", -140)
          .attr("y", -120)
          .attr("width", 200)
          .attr("height", 30)
          .style("fill-opacity", 1);
      executorUserName.append("text")
          .attr("dy", 20)
          .attr("text-anchor", 'left')
          .text(function (d) {
            // TODO 如果没有这个地段 不应显示就给它text设置 或者你可以为空 后面也可以不需要进行对Y 坐标处理 看需求怎么搞
        if (d.data.taskLevel !== 1) {
          if (d.data.executorUserName && d.data.executorUserName.length >= 10) {
            return '处理人:' + d.data.executorUserName.substring(0, 8) + '...'
          } else {
            return '处理人:' + (d.data.executorUserName ? d.data.executorUserName : '无')
          }
        }
      }).style("fill-opacity", 1);
      // TODO 所属组织、执行组织 我这里因为一级任务没有处理人 所以根节点卡片不应该出现该字段 我没使用display属性 而是对y 坐标进行了处理
      rectText.append("text")
          .attr("x", -140)
          .attr("y", (d) => {return d.data.taskLevel === 1 ? -120 : -90})
          .attr("dy", 20)
          .attr("text-anchor", 'left')
          .text((d) => {
           return d.data.taskLevel === 1?' 所属组织:' + (d.data.executorOrgName||'无'):'执行组织 :' + (d.data.dutyOrgName||'无')})
          .style("fill-opacity", 1);
      // 任务开始时间
      rectText.append("text").attr("x", -140).attr("y", (d) => {
        return d.data.taskLevel === 1 ? -90 : -60
      }).attr("dy", 20).attr("text-anchor", 'left').text((d) => {
        return !d.data.taskStartTime?`最近任务开始日期:无`:`最近任务开始日期:${d.data.taskStartTime}`
      }).style("fill-opacity", 1);
      // 任务结束时间
      rectText.append("text")
          .attr("x", -140)
          .attr("y", (d) => {return d.data.taskLevel === 1 ? -60 : -30})
          .attr("dy", 20)
          .attr("text-anchor", 'left')
          .text((d) => {return !d.data.taskEndTime?`最近任务结束日期:无`:`最近任务结束日期:${d.data.taskEndTime}`})
          .style("fill-opacity", 1);
      // TODO 进度条 当任务开始时 没有分配任务按钮 变成一个任务的百分比进度
      let circleText = rectText.append("g").on('click', this.clickCircle).style("cursor", (d) => {
        if (this.isTaskBegin) {
          return d.parent && d.data.executorUserId === d.parent.data.executorUserId ? 'no-drop' : 'pointer';
        } else {
          return 'default'
        }
      }).attr("transform", function (d) {
        return "translate(" + 110 + "," + -130 + ")";
      });
      circleText.append("circle").attr("r", (d) => {
        //圆圈半径大小
        return this.isTaskBegin?26:20;
      }).style("fill", (d) => {
        if (this.isTaskBegin) {
          return d.parent && d.data.executorUserId === d.parent.data.executorUserId ? '#edf2fc' : '#0FA3A5';
        }
        //圈的颜色
        const colorArr={
          1:"#f7c692",
          2:"#FA8C15",
          3:"#64D16D",
          4:"#1592E6",
          5:"#666565",
          null:"#E62412"
        }
        return colorArr[d.data.taskStatus]||colorArr['null'];
      });
      // 进度值
      circleText.append("text").attr("y", (d) => {
        if (this.isTaskBegin) {
          return -2
        } else {
          return 40
        }
      }).attr("text-anchor", 'middle').attr("fill", (d) => {
        if (this.isTaskBegin) return '#fff';
        // 对应 圈下面的百分比值颜色
        const colorArr={
          1:"#f7c692",
          2:"#FA8C15",
          3:"#64D16D",
          4:"#1592E6",
          5:"#666565",
          null:"#E62412"
        }
        return colorArr[d.data.taskStatus]||colorArr['null'];
      })
          .text((d) => {
                if (this.isTaskBegin) return '分配'
                return d.data.feedbackProgress ? Number(d.data.feedbackProgress) * 1000 / 10 + '%' : '0%'
              }
          ).style("fill-opacity", 1);
      circleText.append("text")
          .attr("text-anchor", 'middle')
          .attr("fill", '#fff')
          .attr("y", 13)
          .text('任务')
          .style("display", (d) => {
            return this.isTaskBegin ? 'block' : 'none'
          });
    },
    findTaskTrack() {
      // 调用接口获取数据
      const {code, result, message} = this.simulationData();
      if (code === 200) {
        this.treeData = result;
        //如果有子任务进行递归处理
        this.recursionDispose(result, "subTaskList", (node, level, index) => {
          if (node.userIdList && node.userIdList.length > 100) {
            this.refresh = parseInt(node.userIdList.length / 25);
          }
        });
        this.root = this.getRoot('subTaskList')
        this.update(this.root, 'container')
      }
    },
    /**
     * 递归处理任务
     * @param data 任务数据
     * @param childrenKey 对应子任务属性值
     * @param dispose 回调函数处理
     * @param level 级别
     * @param index 循环索引
     * @param parent 父级数据
     */
    recursionDispose(data, childrenKey, dispose, level, index, parent = null) {
      let l = typeof level === 'number' ? level : 1;
      let i = typeof index === 'number' ? index : 0;
      let children = data && data[childrenKey];
      if (children instanceof Array) {
        for (let j = 0, len = children.length; j < len; j++) {
          this.recursionDispose(children[j], childrenKey, dispose, l + 1, i + 1, data);
        }
      }
      dispose(data, l, i, parent);
    },
    /**
     * 模拟数据
     */
    simulationData(){
      return {
        "code": 200,
        "message": "操作成功",
        "result": {
          "id": null,
          "taskId": "62869132028440166471755569060000",
          "taskName": "井底的蜗牛之爬出井底计划",
          "parentId": "0",
          "superId": "62869132028440166471755569060000",
          "taskLevel": 1,
          "taskStatus": null,
          "taskStartTime": null,
          "taskEndTime": null,
          "userIdList": null,
          "executorUserId": "6170876529206517766172707211783",
          "executorUserName": "井底的蜗牛",
          "executorOrgId": "61708228580701388861705474010582",
          "executorOrgName": "井蜗集团-效率部门",
          "superAuditorName": null,
          "auditorName": null,
          "feedbackProgress": 20,
          "organizationId": null,
          "dutyOrgName": null,
          "subTaskList": [
            {
              "id": null,
              "taskId": "62968505366453862401144414070000",
              "taskName": "井底的蜗牛之爬出井底计划PlaneA",
              "parentId": "62869132028440166471755569060000",
              "superId": "62869132028440166471755569060000",
              "taskLevel": 2,
              "taskStatus": null,
              "taskStartTime": null,
              "taskEndTime": null,
              "userIdList": null,
              "executorUserId": "61815438753855078491605563732019",
              "executorUserName": "井底的蜗牛2号",
              "executorOrgId": "6170867344908369926172328501773",
              "executorOrgName": "如何提升自身速度",
              "superAuditorName": null,
              "auditorName": "井底的蜗牛",
              "feedbackProgress": 10,
              "organizationId": "61708228580701388861705474010582",
              "dutyOrgName": "井蜗集团-发展规划部",
              "subTaskList": [],
            },
            {
              "id": null,
              "taskId": "62972444949060403201421141040000",
              "taskName": "井底的蜗牛之爬出井底计划PlaneB",
              "parentId": "62869132028440166471755569060000",
              "superId": "62869132028440166471755569060000",
              "taskLevel": 2,
              "taskStatus": null,
              "taskStartTime": null,
              "taskEndTime": null,
              "userIdList": null,
              "executorUserId": "61815461448101888091606504802026",
              "executorUserName": "井底的蜗牛3号",
              "executorOrgId": "6170867344908369926172328501773",
              "executorOrgName": "探索井外世界",
              "superAuditorName": null,
              "auditorName": "井底的蜗牛",
              "feedbackProgress": 10,
              "organizationId": "61708228580701388861705474010582",
              "dutyOrgName": "井蜗集团-研究探索部",
              "subTaskList": [],
            }
          ],
        }
      }
    }
  }
}
</script>
<style scoped lang="scss">
.task-container {
  width: 100%;
  height: 100vh;
  position: relative;

}

.tree-container {
  width: 100%;
  height: calc(100% - 46px);
  position: relative;
  overflow: hidden;

  .d3-tree {
    width: 100%;
    height: 100%;
    // .container {
    //     transform: translate(50%, 140px);
    // }
  }

  .tip_content {
    background: #fff;
    border: 2px solid #33a39c;
    position: absolute;
    z-index: 5;
    width: 319px;
    // height: 195px;
    border-radius: 6px;
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
    padding: 10px 20px;

    div {
      padding: 6px 0;
      font-size: 14px;
      flex: 2;
    }

    .arrow_left {
      position: absolute;
      right: -12px;
      top: 40px;
      display: block;
      padding: 0;
      width: 0;
      height: 0;
      border-color: transparent;
      border-style: solid;
      border-width: 12px;
      border-left-color: #33a39c;
      border-right-width: 0;
      filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));

      &::after {
        content: '';
        position: absolute;
        right: 2px;
        top: -12px;
        display: block;
        padding: 0;
        width: 0;
        height: 0;
        border-color: transparent;
        border-style: solid;
        border-width: 12px;
        border-left-color: #fff;
        border-right-width: 0;
        filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
      }
    }

    .arrow_right {
      position: absolute;
      left: -12px;
      top: 40px;
      display: block;
      padding: 0;
      width: 0;
      height: 0;
      border-color: transparent;
      border-style: solid;
      border-width: 12px;
      border-right-color: #33a39c;
      border-left-width: 0;
      filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));

      &::after {
        content: '';
        position: absolute;
        left: 2px;
        top: -12px;
        display: block;
        padding: 0;
        width: 0;
        height: 0;
        border-color: transparent;
        border-style: solid;
        border-width: 12px;
        border-right-color: #fff;
        border-left-width: 0;
        filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
      }
    }
  }
}

.d3-tree {
  .node {
    cursor: pointer;
  }

  .node circle {
    fill: #fff;
    stroke: steelblue;
    stroke-width: 1.5px;
  }

  .node text {
    font: 18px sans-serif;
  }

  .link {
    fill: none;
    stroke: #ccc;
    stroke-width: 1.5px;
  }
}
</style>

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/14560.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信