“手搓”一个可自由编辑的组织架构图

937 阅读3分钟

前言

最近项目中用到了组织架构图这个东西,产品的需求是不仅可以展示,还可以自由编辑,于是就突发奇想,使用canvas手搓一下,目前的功能只有展示和对节点的编辑等操作,后续可能会涉及到拖拽编辑等其他操作

运行效果和全部代码

此处的预览效果菜单有部分偏差,大家可以复制到本地预览

详细步骤

基础架构

  • canvas
  • 菜单

image.png

  • 定义数据结构和节点基础配置
  • 定义鼠标点击的节点

image.png

如何绘制节点

  • 在canvas 上绘制一个矩形需要 起始(x,y)和长宽(在节点基础配置已定义)就行
  • 把节点名字进行分割 循环绘制达到垂直的效果

image.png

绘制两个节点间的连线

  • 两个(x,y)即可
  • 可以使用直线 也可以使用曲线 这个可以自由选择

image.png

计算架构中每一层级的节点数量 同时保留父子关系

  • 计算每层节点数量 我们可以合理的分配区域

image.png

开始绘制架构图 并确定绘制步骤

function drawOrgChart(root) {
        const levelNodes = getLevelNodes(root)
        const nodePositions = []
        // 添加顶部边距常量
        const TOP_MARGIN = 100 // 设置顶部边距为100px
        // 第一步:计算每个父节点下子节点的总宽度
        // 第二步:为每个节点分配位置
        // 计算整个树的总宽度
        const totalTreeWidth = calculateChildrenWidth(root)
        const startX = (canvas.width - totalTreeWidth) / 2

        // 分配位置并绘制
        assignNodePositions(root, 0, startX)
        // 绘制连接线和节点
        
      }

第一步:计算每个父节点下子节点的总宽度

此处的思路是保证 父节点的位置在子节点的中间(即父节点的位置由子节点决定 因为需要递归) 没有子节点的元素不需要加上nodeConfig.horizontalGap image.png 此处不加nodeConfig.horizontalGap image.png 此处加nodeConfig.horizontalGap

image.png

第二步:为每个节点分配位置

image.png 此处秉持的思路是 把所有节点落到同一层,在次层级计算出该节点所对应的x坐标(y坐标根据层级来的,可以认为已经确定) image.png

绘制连接线和节点

此时我们需要一个函数去寻找当前节点的父节点

image.png

对定义的nodePositions 进行循环绘制节点

image.png


此时绘制工作已经完成了 无需对节点编辑的话就到此结束了

添加右键菜单和点击菜单

绑定监听函数

 canvas.addEventListener('contextmenu', (e) => {
        e.preventDefault()
        e.stopPropagation()
 })
 canvas.addEventListener('click', (e) => {
        e.stopPropagation() // 阻止事件冒泡
 })
  document.addEventListener('click', (e) => {
        // 如果点击的是 canvas 区域,不要在这里处理
        if (e.target === canvas) {
          return
        }

        // 如果点击的不是菜单内部,则隐藏菜单
        if (!addMenu.contains(e.target) && !deleteMenu.contains(e.target)) {
          hideMenus()
        }
      })

添加菜单

canvas.addEventListener('click', (e) => {
        e.stopPropagation() // 阻止事件冒泡

        const rect = canvas.getBoundingClientRect()
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        //找到点击的节点
        selectedNode = findNode(x, y)

        // 无论如何,先隐藏所有菜单
        hideMenus()

        if (selectedNode) {
          // 显示添加菜单
          addMenu.style.display = 'block'

          // 计算菜单位置,避免超出窗口边界
          let menuX = e.pageX
          let menuY = e.pageY

          const menuWidth = addMenu.offsetWidth
          const menuHeight = addMenu.offsetHeight
          const windowWidth = window.innerWidth
          const windowHeight = window.innerHeight

          if (menuX + menuWidth > windowWidth) {
            menuX = windowWidth - menuWidth
          }
          if (menuY + menuHeight > windowHeight) {
            menuY = windowHeight - menuHeight
          }

          addMenu.style.left = menuX + 'px'
          addMenu.style.top = menuY + 'px'
        }
      })
  

找到点击的节点

image.png

绑定菜单的处理函数

image.png

添加子节点

image.png

添加同级节点

image.png

编辑节点

image.png

在操作后重新渲染

image.png

结语

谢谢你的观看 如果你觉得不错,点个赞就是对我最大的支持,如果你有什么问题欢迎在评论区留言 另外可拖拽的节点文章正在编写,敬请期待