vue3中使用Antv G6渲染树形结构并支持节点增删改

写在前面

在一些管理系统中,会对组织架构、级联数据等做一些管理,你会怎么实现呢?在经过调研很多插件之后决定使用 Antv G6 实现,文档也比较清晰,看看怎么实现吧,先来看看效果图。点击在线体验

实现的功能有:

  1. 增加节点
  2. 删除节点
  3. 编辑节点
  4. 展开收起

具体实现

  1. 先在项目中安装 antv g6
shell 复制代码
npm install --save @antv/g6
  1. vue 文件创建容器渲染
  • 渲染的容器
vue 复制代码
<div id="container" class="one-tree"></div>
  • 渲染方法和初始化树图
js 复制代码
import G6 from '@antv/g6'

const state = reactive({
    treeData: {
        id: 'root',
        sname: 'root',
        name: uniqueId(),
        children: [],
    },
    graph: null,
})

function renderMap(data: any[], graph: Graph): void {
  G6.registerNode(
    'icon-node',
    {
      options: {
        size: [60, 20],
        stroke: '#73D13D',
        fill: '#fff'
      },
      draw(cfg: any, group: any) {
        const styles = (this as any).getShapeStyle(cfg)
        const { labelCfg = {} } = cfg

        const w = cfg.size[0]
        const h = cfg.size[1]
        const keyShape = group.addShape('rect', {
          attrs: {
            ...styles,
            cursor: 'pointer',
            x: 0,
            y: 0,
            width: w, // 200,
            height: h, // 60
            fill: cfg.style.fill || '#fff'
          },
          name: 'node-rect',
          draggable: true
        })

        // 动态增加和删除元素
        group.addShape('text', {
          attrs: {
            x: 131,
            y: 20,
            r: 6,
            stroke: '#707070',
            cursor: 'pointer',
            opacity: 1,
            fontFamily: 'iconfont',
            textAlign: 'center',
            textBaseline: 'middle',
            text: '\ue658',
            fontSize: 16
          },
          name: 'add-item'
        })
        // 删除icon,根元素不能删除
        if (cfg.id !== 'root') {
          group.addShape('text', {
            attrs: {
              x: 110,
              y: 20,
              r: 6,
              fontFamily: 'iconfont',
              textAlign: 'center',
              textBaseline: 'middle',
              text: '\ue74b',
              fontSize: 14,
              stroke: '#909399',
              cursor: 'pointer',
              opacity: 0
            },
            name: 'remove-item'
          })
        }

        if (cfg.sname) {
          group.addShape('text', {
            attrs: {
              ...labelCfg.style,
              text: fittingString(cfg.sname, 110, 12),
              textAlign: 'left',
              x: 10,
              y: 25
            }
          })
        }
        // 展开收起
        if (cfg.children && cfg.children.length > 0) {
          group.addShape('circle', {
            attrs: {
              width: 24,
              height: 24,
              x: 154,
              y: 20,
              r: 12,
              cursor: 'pointer',
              lineWidth: 1,
              fill: !cfg.collapsed ? '#9e9e9e' : '#2196f3',
              opacity: 1,
              text: 1
            },
            name: 'collapse-icon'
          })
          group.addShape('text', {
            attrs: {
              ...labelCfg.style,
              text: cfg.children.length,
              textAlign: 'left',
              x: 150,
              y: 25,
              fill: '#ffffff',
              fontWeight: 500,
              cursor: 'pointer'
            },
            name: 'collapse-icon'
          })
        }
        return keyShape
      },
      setState(name, value, item) {
        const group = item?.getContainer()
        if (name === 'collapsed') {
          const marker = item?.get('group').find((ele: any) => ele.get('name') === 'collapse-icon')
          const icon = value ? G6.Marker.expand : G6.Marker.collapse
          marker.attr('symbol', icon)
        }
        if (name === 'selected') {
          const nodeRect = group?.find(function (e) {
            return e.get('name') === 'node-rect'
          })
          if (value) {
            nodeRect?.attr({
              stroke: '#2196f3',
              lineWidth: 2
            })
          }
        }
        if (name === 'hover') {
          const addMarker = group?.find(function (e) {
            return e.get('name') === 'add-item'
          })
          const reduceMarker = group?.find(function (e) {
            return e.get('name') === 'remove-item'
          })
          if (value) {
            addMarker?.attr({
              opacity: 1
            })
            reduceMarker?.attr({
              opacity: 1
            })
          }
        }
      },
      update: undefined
    },
    'rect'
  )
  graph.data(data)
  graph.render()
  mouseenterNode(graph)
  mouseLeaveNode(graph)
  collapseNode(graph)
}

function initGraph(graphWrapId: string): Graph {
  const width = (document.getElementById(graphWrapId) as HTMLElement).clientWidth || 1000
  const height = (document.getElementById(graphWrapId) as HTMLElement).clientHeight || 1000
  const graph = new G6.TreeGraph({
    container: graphWrapId,
    width,
    height,
    linkCenter: true,
    animate: false,
    fitView: false, // 自动调整节点位置和缩放,使得节点适应画布大小
    modes: {
      default: ['scroll-canvas'],
      edit: ['click-select']
    },
    defaultNode: {
      type: 'icon-node',
      size: [120, 40],
      style: defaultNodeStyle,
      labelCfg: defaultLabelCfg
    },
    defaultEdge: {
      type: 'cubic-vertical'
    },
    comboStateStyles,
    layout: defaultLayout
  })
  return graph
}
  • 事件处理
js 复制代码
/**
 * @description:树型图的事件绑定
 */

// 展开收起子节点
function collapseNode(graph: Graph): void {
    // 展开和收起子节点
    graph.on('node:click', (e: any) => {
        if (e.target.get('name') === 'collapse-icon') {
            e.item.getModel().collapsed = !e.item.getModel().collapsed
            graph.setItemState(e.item, 'collapsed', e.item.getModel().collapsed)
            graph.layout()
        }
    })
}

// 鼠标滑入
function mouseenterNode(graph: Graph): void {
    graph.on('node:mouseover', (evt: any) => {
        const { item, target } = evt
        if (item._cfg.id === 'root') return
        const canHoverName = ['node-rect', 'remove-item']
        if (!canHoverName.includes(target.get('name'))) return

        // 显示icon
        const deleteItem = item.get('group').find(function (el: any) {
            return el.cfg.name === 'remove-item'
        })
        deleteItem.attr('opacity', 1)
        if (item._cfg && item._cfg.keyShape) {
            item._cfg.keyShape.attr('stroke', '#2196f3')
        }
        graph.setItemState(item, 'active', true)
    })
}

// 鼠标离开
function mouseLeaveNode(graph: Graph): void {
    graph.on('node:mouseout', (evt: any) => {
        const { item, target } = evt
        const canHoverName = ['node-rect', 'remove-item']
        if (item._cfg.id === 'root') return
        if (!canHoverName.includes(target.get('name'))) return
        // 隐藏icon
        const deleteItem = item.get('group').find(function (el: any) {
            return el.cfg.name === 'remove-item'
        })
        deleteItem.attr('opacity', 0)
        if (item._cfg && item._cfg.keyShape) {
            item._cfg.keyShape.attr('stroke', '#fff')
        }
        graph.setItemState(item, 'active', false)
    })
}

/**
 * @description 文本超长显示
 */
const fittingString = (str: string, maxWidth: number, fontSize: number): string => {
    const ellipsis = '...'
    const ellipsisLength = Util.getTextSize(ellipsis, fontSize)[0]
    let currentWidth = 0
    let res = str
    const pattern = new RegExp('[\u4E00-\u9FA5]+')
    str.split('').forEach((letter, i) => {
        if (currentWidth > maxWidth - ellipsisLength) return
        if (pattern.test(letter)) {
            currentWidth += fontSize
        } else {
            currentWidth += Util.getLetterWidth(letter, fontSize)
        }
        if (currentWidth > maxWidth - ellipsisLength) {
            res = `${str.substr(0, i)}${ellipsis}`
        }
    })
    return res
}
  • 节点的增加、删除、编辑时间
js 复制代码
const addEvent = (graph: any) => {
    graph.on('node:click', (evt: any) => {
        const { item, target } = evt
        const name = target.get('name')

        // 增加元素
        const model = item.getModel()
        if (name === 'add-item') {
            state.editType = 'add'
            // 如果收起需要展开
            if (model.collapsed) model.collapsed = false
            // 没有子级的时候设置空数组
            if (!model.children) model.children = []
            const id = uniqueId()
            model.children.push({
                id,
                name: 1,
                sname: '',
                parentId: model.id,
            })
            graph.updateChild(model, model.id)
            const curTarget = graph.findDataById(id)
            const canvasXY = graph.getCanvasByPoint(curTarget.x, curTarget.y)
            state.editOne = curTarget
            state.input = curTarget.sname
            setTimeout(() => {
                state.showInput = true
                nextTick(() => {
                    inputRref.value.focus()
                })
            }, 200)
            // 更改输入框的位置
            state.inputStyle = {
                left: `${canvasXY.x}px`,
                top: `${canvasXY.y}px`,
            }
        }
        // 删除节点
        if (name === 'remove-item') {
            graph.removeChild(model.id)
            // 查找当前的父id,更新其子元素的长度
            graph.updateItem(model.parentId, {})
        }

        // 编辑
        if (name === 'node-rect') {
            const curTarget = graph.findDataById(item._cfg.id)
            const canvasXY = graph.getCanvasByPoint(curTarget.x, curTarget.y)
            state.editOne = evt.item
            state.input = curTarget.sname
            state.showInput = true
            state.editType = 'edit'
            nextTick(() => {
                inputRref.value.focus()
            })
            state.inputStyle = {
                left: `${canvasXY.x}px`,
                top: `${canvasXY.y}px`,
            }
        }
    })
    // 画布滚动、拖动时,不能编辑节点名称
    graph.on('dragstart', () => {
        state.showInput = false
    })
    graph.on('wheel', () => {
        state.showInput = false
    })
}
  • dom 节点渲染后渲染树图
js 复制代码
onMounted(() => {
    nextTick(() => {
        state.graph = initGraph('container')
        state.graph.clear()
        addEvent(state.graph)
        renderMap(state.treeData, state.graph)
    })
})

相关链接

  1. 源码链接
  2. Antv G6 官网
  3. 参考文章
相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端