vue+d3.js实现组织架构图

1,265 阅读1分钟

最近接到一个需求,实现组织架构图,领导说,你去研究下思维导图,用思维导图实现。 于是我一古脑的就研究了思维导图,研究来研究去,总感觉有点地方不对。 我告诉领导,我研究的思维导图智能化左右不能不能上下啊。领导说那不行,继续研究。 于是我对着销售给来的截图10分钟,才发现这不是组织架构图吗?领导说是思维导图。

所以,领导有时候也是猪啊。

翻遍组织架构插件,都不能完全实现我们的需求,于是决定用d3结合自己来写。 上代码:

<template>
  <div ref="orgTreeContainer" class="org-tree-wrap">
    <div id="treeSvg"></div>
  </div>
</template>

<script>
  import * as d3 from 'd3'
  export default {
    name: 'CommonOrgTree',
    props: {
      data: {
        type: Object,
        default: () => {
          return {}
        },
      },
      url: {
        type: String,
        default: '',
      },
      svgConfig: {
        type: Object,
        default: () => {
          return {
            y: 40, //y轴下移40px
          }
        },
      },
      arrowConfig: {
        type: Object,
        default: () => {
          return { color: '#409eff' }
        },
      },
      circleConfig: {
        type: Object,
        default: () => {
          return {
            r: 7,
            padding: 3,
            fillColor: '#409eff',
            strokeColor: '#fff',
          }
        },
      },
      linkConfig: {
        type: Object,
        default: () => {
          return {
            lineColor: '#409eff',
            textColor: '#409eff',
            height: 140,
            textField: 'GDP',
          }
        },
      },
      nodeConfig: {
        type: Object,
        default: () => {
          return {
            fields: ['name', 'id', 'GDP'],
            height: 75,
            width: 100,
          }
        },
      },
    },
    setup(props) {
      const DEFAULTSVGCONFIG = {
        y: 40,
      }
      const DEFAULTARROWCONFIG = {
        color: '#409eff',
      }
      const DEFAULTCIRCLECONFIG = {
        r: 7,
        padding: 3,
        fillColor: '#409eff',
        strokeColor: '#fff',
      }
      const DEFAULTLINKCONFIG = {
        lineColor: '#409eff',
        textColor: '#409eff',
        height: 140,
      }
      const DEFAULTNODECONFIG = {
        fields: ['name', 'id', 'GDP'],
        height: 75,
        width: 100,
      }
      let treeData = null
      const orgTreeContainer = ref(null)
      let treeMap = null
      let svg = null
      let timeContaniner = null

      /**
       * 获取树结构深度
       */
      const getTreedeep = (treeData, sum = 0) => {
        let sums = []
        if (treeData.children) {
          sums = treeData.children.map((item) => {
            const sumC = sum
            if (item.children) {
              return getTreedeep(item, sumC + 1)
            } else {
              return sumC + 1
            }
          })
        }
        const value = sums.reduce((pre, cur) => {
          if (cur > pre) return cur
          return pre
        })
        return value
      }

      /**
       *创建箭头
       * @param {*} svg  初始化创建的svg
       */
      const createMark = () => {
        const { arrowConfig = DEFAULTARROWCONFIG } = props
        const marker = svg
          .append('marker')
          .attr('id', 'resolved')
          .attr('markerUnits', 'strokeWidth') //设置为strokeWidth箭头会随着线的粗细发生变化
          .attr('markerUnits', 'userSpaceOnUse')
          .attr('viewBox', '0 -5 10 10') //坐标系的区域
          .attr('refX', 10) //箭头坐标
          .attr('refY', 0)
          .attr('markerWidth', 12) //标识的大小
          .attr('markerHeight', 12)
          .attr('orient', 'auto') //绘制方向,可设定为:auto(自动确认方向)和 角度值
          .attr('stroke-width', 2) //箭头宽度
          .append('path')
          .attr('d', 'M0,-5L10,0L0,5') //箭头的路径
          .attr('fill', arrowConfig.color) //箭头颜色

        return marker
      }

      /**
       * 节点收缩子节点符号
       * @param {*} node 节点
       */
      const drawCircle = (node) => {
        const { r, padding, fillColor, strokeColor } = {
          ...props.circleConfig,
          ...DEFAULTCIRCLECONFIG,
        }
        const { width, height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
        const gMark = node
          .append('g')
          .style('display', (d) => {
            if (!d.data._children) {
              return 'none'
            }
          })
          .attr('class', 'node-circle')
          .attr('transform', `translate(${width / 2},${height + r})`)

        gMark
          .append('circle')
          .attr('fill', 'none')
          .attr('r', (d) => (d.depth === 0 ? 0 : r)) //根节点不设置圆圈
          .attr('fill', fillColor)
        gMark
          .append('path')
          .attr('d', `m -${padding} 0 l ${2 * padding} 0`)
          .attr('stroke', strokeColor) //横线

        gMark
          .append('path') //竖线,根据展开/收缩动态控制显示
          .attr('d', `m 0 -${padding} l 0 ${2 * padding}`)
          .attr('stroke-width', 1)
          .attr('stroke', strokeColor)
          .attr('class', 'node-circle-vertical')
        return gMark
      }

      /**
       * 点击节点展开收缩子节点
       * @param {*} d
       */
      const clickNode = (d) => {
        if (!d.data._children && !d.data.children) {
          //无子节点
          return
        }
        if (d.data.children) {
          d.data._children = d.data.children
          d.data.children = null
        } else {
          d.data.children = d.data._children
          d.data._children = null
        }
        //**清除画布 */
        d3.select('#treeSvg').selectAll('svg').remove()
        updateTree()
      }

      /**
       * 初始化树
       */
      const initSvgTree = () => {
        const { y } = { ...props.svgConfig, ...DEFAULTSVGCONFIG }
        const { height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
        const { height: lineHeight } = {
          ...props.linkConfig,
          ...DEFAULTLINKCONFIG,
        }
        if (!orgTreeContainer.value) return
        const { offsetWidth, offsetHeight } = orgTreeContainer.value
        const treeDepth = getTreedeep(treeData)
        const treeHeight = treeDepth * lineHeight + height
        const curHeight = Math.max(treeHeight, offsetHeight)
        treeMap = d3.tree().size([offsetWidth - 50, curHeight])
        svg = d3
          .select('#treeSvg')
          .append('svg')
          .attr('width', offsetWidth)
          .attr('height', curHeight + height)
          .append('g')
          .attr('transform', `translate(0,${y})`)
          .attr('width', offsetWidth)
          .attr('height', curHeight + y)
      }

      /**
       * 创建节点div
       * @param {*} d
       */
      function createNodeDiv(d) {
        const { html, fields } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
        if (!html) {
          let str = ''
          const dom = fields.reduce((pre, cur, index) => {
            let strHtml = ''
            if (index === 0) {
              strHtml = `<div class="title">${d.data[cur] ?? ''}</div>`
            } else {
              strHtml += `${pre}<div class="des">${d.data[cur] ?? ''}</div>`
            }
            return strHtml
          }, '')
          str = `<div class="org-content-wrap "> ${dom}</div>`
          return str
        }
        const html1 = html.replace(/\{\{(\w+|d+)\}\}/g, (word, $1) => {
          console.warn(6859869, word, $1)
          return d.data[$1] ?? ''
        })
        return html1
      }

      /**
       * 创建links
       */
      const createLinks = (nodes) => {
        const { lineColor, textColor, textField } = {
          ...props.linkConfig,
          ...DEFAULTLINKCONFIG,
        }
        const links = nodes.links()
        const link = svg.selectAll('.link').data(links)

        link
          .enter()
          .append('path')
          .attr('class', 'link')
          .attr('fill', 'none')
          .attr('stroke-width', 1)
          .attr('stroke', lineColor)
          .attr(
            'd',
            d3
              .linkVertical() // linkVertical() 垂直  linkHorizontal() 水平
              .x(function (d) {
                return d.x + 20
              })
              .y(function (d) {
                return d.y
              })
          )
          .attr('marker-end', 'url(#resolved)')

        link
          .enter()
          .append('g')
          .attr('transform', function (d) {
            const x = d.target.x + 10
            const y = d.target.y - 40
            return `translate(${x},${y})`
          })
          .append('text')
          .attr('dy', '.33em')
          .attr('font-size', '12px')
          .attr('fill', textColor)
          .attr('writing-mode', 'tb')
          .text(function (d) {
            return d.target.data[textField]
          })
      }

      /**
       * 创建nodes
       * @param {*} nodes
       */
      const createNodes = (nodesDes) => {
        const { width, height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
        const node = svg
          .selectAll('.node')
          .data(nodesDes)
          .enter()
          .append('g')
          .attr('class', function (d) {
            return `node${d.children ? ' node--internal' : ' node--leaf'}`
          })
          .attr('transform', function (d) {
            const x = d.x - 25
            const y = d.y
            return `translate(${x},${y})`
          })
          .on('click', ($event, d) => {
            d.depth !== 0 && clickNode(d) //根节点不执行点击事件
          })

        node
          .append('foreignObject')
          .attr('class', (d) => {
            const className = d.depth === 0 ? 'first-org-oreign' : ''
            return `org-oreign ${className}`
          })
          .attr('width', width)
          .attr('height', height)
          .append('xhtml:div')
          .html((d) => {
            return createNodeDiv(d)
          })

        drawCircle(node)
      }

      /**
       * 创建树以及节点
       */
      function updateTree() {
        const { height } = props.linkConfig
        initSvgTree()
        //箭头
        createMark(svg)

        const dataSet = d3.hierarchy(treeData)
        const nodes = treeMap(dataSet)
        const nodeInfos = nodes.descendants()
        nodeInfos.forEach((d) => {
          d.y = height * d.depth
        })

        //获取link以及创建link
        createLinks(nodes)

        //获取node以及创建node
        createNodes(nodeInfos)
      }

      onUnmounted(() => {
        clearTimeout(timeContaniner)
      })

      watch(
        () => props.data,
        (newVal) => {
          if (!props.url) {
            treeData = newVal
            nextTick(() => {
              timeContaniner = setTimeout(() => {
                updateTree()
              }, 350)
            })
          }
        },
        {
          deep: true,
          immediate: true,
        }
      )

      return {
        orgTreeContainer,
      }
    },
  }
</script>
<style>
  .org-tree-wrap {
    flex: 1;
  }
  .foreign_add {
    width: 10px;
    height: 10px;
    border-radius: 5px;
  }

  .org-oreign {
    padding: 0 10px;
    font-size: 12px;
    background-color: #4ae;
    border-radius: 5px;
  }

  .first-org-oreign {
    position: relative;
    transform: translateY(-40px);
  }

  .org-oreign div {
    height: 25px;
    line-height: 25px;
  }

  .org-oreign .title {
    font-weight: bold;
  }
</style>

整体思路为:创建画布,初始化树,创建连线,创建node,且可折叠子节点 最后展示效果:

image.png