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

news2024/11/16 19:49:29

写在前面

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

效果图
实现的功能有:

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

具体实现

  1. 先在项目中安装 antv g6
npm install --save @antv/g6
  1. vue 文件创建容器渲染
  • 渲染的容器
<div id="container" class="one-tree"></div>
  • 渲染方法和初始化树图
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
}
  • 事件处理
/**
 * @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
}
  • 节点的增加、删除、编辑时间
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 节点渲染后渲染树图
onMounted(() => {
    nextTick(() => {
        state.graph = initGraph('container')
        state.graph.clear()
        addEvent(state.graph)
        renderMap(state.treeData, state.graph)
    })
})

相关链接

  1. 源码链接
  2. Antv G6 官网
  3. 参考文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1884820.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图形的搭建

例一&#xff1a; 输入描述&#xff1a; 多组输入&#xff0c;一个整数&#xff08;2~20&#xff09;&#xff0c;表示输出的行数&#xff0c;也表示组成“X”的反斜线和正斜线的长度。 输出描述&#xff1a; 针对每行输入&#xff0c;输出用“*”组成的X形图案。 示例一&…

macos m2 百度paddleocr文字识别 python

创建了一个虚拟环境&#xff1a;conda create -n orc python3.11.7 进入虚拟环境后执行2条命令 pip install paddleocr -i https://pypi.tuna.tsinghua.edu.cn/simple pip install paddlepaddle -i https://pypi.tuna.tsinghua.edu.cn/simple​ ​ 安装好后&#xff0c;在网…

VUE2及其生态查漏补缺

1、数据代理概括 数据代理过程相当于是进行了 vm 代理 vm_data中的属性&#xff0c;vm._data 是与 我们vue文件中写的 data是全等的 //创建Vue实例let data { //data中用于存储数据&#xff0c;数据供el所指定的容器去使用&#xff0c;值我们暂时先写成一个对象。name:atguig…

[C++][设计模式][迭代器模式]详细讲解

目录 1.动机2.模式定义3.要点总结4.代码感受 1.动机 在软件构建过程中&#xff0c;集合对象内部结构常常变化各异。但对于这些集合对象&#xff0c;我们希望不暴露其内部结构的同时&#xff0c;可以让外部客户代码透明地访问其中包含的元素&#xff1b; 同时这种”透明遍历“也…

每天五分钟深度学习:解决for循环效率慢的关键在于向量化

本文重点 上一节课程中,我们学习了多样本的线性回归模型,但是我们的伪代码实现中使用了大量的for循环,这样代码的问题是效率很低。为了克服这一瓶颈,向量化技术应运而生,成为提升程序执行效率、加速数据处理速度的重要手段。 向量化技术概述 向量化(Vectorization)是…

DC/AC电源模块:为智能家居设备提供恒定的电力供应

BOSHIDA DC/AC电源模块&#xff1a;为智能家居设备提供恒定的电力供应 DC/AC电源模块是一种常见的电源转换器&#xff0c;它将直流电源&#xff08;DC&#xff09;转换为交流电源&#xff08;AC&#xff09;&#xff0c;为智能家居设备提供恒定的电力供应。在智能家居系统中&a…

用 AI 生成绘本,含大量 prompt

画图过程&#xff0c;为了保证绘本输出的风格统一&#xff0c;角色连贯&#xff0c;画面内容与故事保持一致 1、画风统一的解决办法&#xff1a;固定一个插画师的风格&#xff0c;可以输入插画师的名字&#xff0c;或者垫图&#xff0c;即上传你需要借鉴风格的图片 2、角色连贯…

探索IT世界的第一步:高考后的暑期学习指南

目录 前言1. IT领域概述1.1 IT领域的发展与现状1.2 IT领域的主要分支1.2.1 软件开发1.2.2 数据科学1.2.3 网络与安全1.2.4 系统与运维 2. 学习路线图2.1 基础知识的学习2.1.1 编程语言2.1.2 数据结构与算法 2.2 实战项目的实践2.2.1 个人项目2.2.2 团队项目 2.3 学习资源的利用…

高考假期预习指南,送给迷茫的你

高考结束&#xff0c;离别了熟悉的地方&#xff0c;踏上远方。 你&#xff0c;&#xff0c;迷茫吗&#xff1f; 大学是什么&#xff1f;到了大学我该怎样学习&#xff1f;真像网上说的毕业即失业吗&#xff1f; 大学是一个让你学会一技之长的地方&#xff0c;到了大学找到自…

非平稳信号的自适应局部迭代滤波(MATLAB)

仍以滚动轴承故障诊断为例&#xff0c;在滚动轴承的运行过程中&#xff0c;其振动信号包含了大量的系统运行状态信息。利用振动信号进行滚动轴承的故障诊断&#xff0c;实际上就是分析振动信号、提取信息的过程。由于非线性力的作用&#xff0c;滚动轴承的故障信号往往是非平稳…

为什么需要做网络安全服务?

网络安全服务之所以重要&#xff0c;是因为它在保护数字资产、维护企业运营、确保法规遵从、防范恶意行为以及建立信任等方面扮演着关键角色。以下是一些主要的理由&#xff1a; 保护核心资产和数据&#xff1a; 数字化转型使得企业数据变得极其宝贵&#xff0c;包括知识产权、…

离散模态信息作为细粒度标记用于多模态知识图谱补全--MyGO

多模态知识图谱&#xff08;MMKG&#xff09;存储了包含有丰富的多模态描述信息的、结构化的世界知识。为了克服其固有的不完整性&#xff0c;多模态知识图谱补全&#xff08;MMKGC&#xff09;希望利用三元组的结构信息及实体的多模态信息&#xff0c;从给定的MMKG中发掘未观察…

智能工厂解决方案—智能数据采集平台

智慧工厂总体架构图 由智能化生产、智能化管理和产业链互联三个层面构成,前两个层面立足于企业自身,以智能工厂为建设目标,实习企业机体自身的智能化,而产业链互联则是以互联网技术为基础,将企业融入到产业链的整体生态环境中,逐步实现制造资源的服务化和云化,并与生态系…

爬数据是什么意思?

爬数据的意思是&#xff1a;通过网络爬虫程序来获取需要的网站上的内容信息&#xff0c;比如文字、视频、图片等数据。网络爬虫&#xff08;网页蜘蛛&#xff09;是一种按照一定的规则&#xff0c;自动的抓取万维网信息的程序或者脚本。 学习一些爬数据的知识有什么用呢&#x…

SAP 接口-银行账号主数据维护接口【MDM->SAP】开发说明书(包括测试样例、程序代码仅作参考,不保证一定可以运行)

接口映射字段 开发通用说明 根据MDM传输字段调用BAPI生成银行账号及开户行。 开户行维护BAPI【BAPI_BANK_CREATE】 银行账号维护BAPI【BAPI_FCLM_BAM_AMD_BNKANT】 接口字段【ZZZH 主账户标识】=1时字段【DTAAI】DME标识赋值:常用; 接口字段【ZZZH 主账户标识】=0时字段…

mysql中的递归函数recursive

递归部门 WITH recursive dept_tree AS (SELECTsd.mine_id AS mine_id,sd.dept_id AS dept_id,sd.tenant_id AS tenant_id,sd.order_num,sd.dept_name AS topName,sd.dept_id AS topIdFROMsys_dept sdWHERE<!-- 加上or后也会查询出dept节点 sd.parent_id #{deptId} or sd.…

关于电商平台分类||电商平台商品分类接口|电商平台商品数据

电商平台 做电商&#xff0c;则要有电商平台&#xff0c;一个为 企业 或 个人 提供网上交易洽谈的平台。. 企业电子商务平台是建立在 Internet 网上进行商务活动的虚拟网络空间和保障商务顺利运营的管理环境&#xff1b;是协调、整合 信息流 、货物流、 资金流 有序、关联、高效…

【深度学习】pytorch训练中的一个大坑

使用的命令&#xff1a;iostat -x 5 可以看到 ssd的利用率已经满了。 之前在的数据集放在了 hdd上&#xff0c;训练结果特别慢。 所以我把它移动到了ssd上&#xff0c;然后训练参数用的 resume&#xff0c; 但是&#xff01;&#xff01;&#xff01;&#xff01;它把历史记住…

音乐:触动心灵的艺术语言

Enjoy your music 音 乐 作为一种跨越时空和文化的艺术形式&#xff0c;拥有着无穷的魅力和力量。 它不仅能够带给我们愉悦的听觉享受&#xff0c;还对我们的身心健康、认知发展和社会交往产生着深远的影响。 一、音乐的基本元素 音乐由多个基本元素构成&#xff0c;包括…

【MySQL备份】Percona XtraBackup全量备份实战篇

目录 1. 前言 2.准备工作 2.1.创建备份目录 2.2.配置/etc/my.cnf文件 2.3.授予root用户BACKUP_ADMIN权限 3.全量备份 4.准备备份 5.数据恢复 6.总结 "实战演练&#xff1a;利用Percona XtraBackup执行MySQL全量备份操作详解" 1. 前言 本文将继续上篇【My…