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

写在前面

在一些管理系统中,会对组织架构、级联数据等做一些管理,你会怎么实现呢?在经过调研很多插件之后决定使用 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. 参考文章

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

仓颉——申请内测、环境搭建、编译测试

2024年6月21日&#xff0c;华为仓颉正式公开发布。 不少同学看过仓颉白皮书后&#xff0c;都在找SDK从哪下载&#xff0c;HelloWorld怎么跑。仓颉公众号也及时发布了内测的方式&#xff0c;我也亲自走了一遍整个流程&#xff0c; 一&#xff0c;申请内测 关注“仓颉编程语言…

香橙派AIpro做目标检测

使用香橙派AIpro做目标检测 文章目录 使用香橙派AIpro做目标检测香橙派AIpro开发板介绍香橙派AIpro应用体验YOLOV5s目标检测使用场景描述图像目标检测视频目标检测摄像头目标检测YOLOv5s 目标检测的运行结果分析香橙派 AIpro 在运行过程中的表现 香橙派AIpro AI应用场景总结 香…

leetCode-hot100-动态规划专题

动态规划 动态规划定义动态规划的核心思想动态规划的基本特征动态规划的基本思路例题322.零钱兑换53.最大子数组和72.编辑距离139.单词拆分62.不同路径63.不同路径Ⅱ64.最小路径和70.爬楼梯121.买卖股票的最佳时机152.乘积最大子数组 动态规划定义 动态规划&#xff08;Dynami…

Python 项目依赖离线管理 pip + requirements.txt

背景 项目研发环境不支持联网&#xff0c;无法通过常规 pip install 来安装依赖&#xff0c;此时需要在联网设备下载依赖&#xff0c;然后拷贝到离线设备进行本地安装。 两台设备的操作系统、Python 版本尽可能一致。 离线安装依赖 # 在联网设备上安装项目所需的依赖 # -d …

香港即将“放松”加密货币监管!加密牌照制度备受批评!全球主力军无法进入香港市场?动摇了香港Web3的信心!

2024年7月3日&#xff0c;香港金融服务及库务局局长许正宇在立法会会议上表示&#xff0c;香港金融管理局(HKMA)和证券及期货事务监察委员会(SFC)将根据市场发展情况&#xff0c;适时检讨虚拟资产相关活动的监管要求。 这一表态引发了人们对香港加密货币监管框架可能进行调整的…

AI智能音箱用2×15W立体声功放芯片NTP8918

智能音箱是近年来非常受欢迎的智能家居产品之一&#xff0c;它集成了人工智能技术和音频技术&#xff0c;能够为用户提供语音助手、音乐播放、智能家居控制等多种功能。其中&#xff0c;音频输出是智能音箱的核心功能之一&#xff0c;而功放芯片则是实现音频放大的关键组成部分…

尽量不写一行if...elseif...写出高质量可持续迭代的项目代码

背景 无论是前端代码还是后端代码&#xff0c;都存在着定位困难&#xff0c;不好抽离&#xff0c;改造困难的问题&#xff0c;造成代码开发越来越慢&#xff0c;此外因为代码耦合较高&#xff0c;总是出现改了一处地方&#xff0c;然后影响其他地方&#xff0c;要么就是要修改…

Liunx网络配置

文章目录 一、查看网络配置永久修改网卡临时修改网卡 二、查看主机名称 hostname三、查看路由表条目 route四、查看网络连接情况netstat五、获取socket统计信息ss六、查看当前系统中打开的文件和进程的工具lsof七、测试网络连通性ping八、跟踪数据包 traceroute九、域名解析 ns…

适合金融行业的国产传输软件应该是怎样的?

对于金融行业来说&#xff0c;正常业务开展离不开文件传输场景&#xff0c;一般来说&#xff0c;金融行业常用的文件传输工具有IM通讯、邮件、自建文件传输系统、FTP应用、U盘等&#xff0c;这些传输工具可以基础实现金融机构的文件传输需求&#xff0c;但也存在如下问题&#…

ONNXRuntime与CUDA所对应的版本

官方链接&#xff1a; NVIDIA - CUDA | onnxruntime

软信天成:您的数据仓库真的“达标”了吗?

在复杂多变的数据环境中&#xff0c;您的数据仓库是否真的“达标”了&#xff1f;本文将深入探讨数据仓库的定义、合格标准及其与数据库的区别&#xff0c;帮助您全面审视并优化您的数据仓库。 一、什么是数据仓库&#xff1f; 数据仓库是一个面向主题的、集成的、相对稳定的、…

Django任意URL跳转漏洞(CVE-2018-14574)

目录 Django介绍 URL跳转漏洞介绍 Django任意URL跳转漏洞介绍 环境搭建 防御方法 前段时间在面试时&#xff0c;问到了URL跳转漏洞&#xff0c;我没有回答好&#xff0c;下午把URL跳转漏洞学习了&#xff0c;发现也不难&#xff0c;看来还需要学习的东西很多呀&#xff0c…

AI 芯片之战:开启智能新时代的关键角逐

在科技发展的浪潮中&#xff0c;一场围绕 AI 芯片的激烈竞争正在全球范围内如火如荼地展开。多家巨头纷纷投身其中&#xff0c;使得这场混战已然进入白热化阶段。 AI 芯片&#xff0c;作为推动人工智能发展的核心硬件&#xff0c;其作用举足轻重。它能够高效地处理海量的数据&a…

# Kafka_深入探秘者(9):kafka 集群管理

Kafka_深入探秘者&#xff08;9&#xff09;&#xff1a;kafka 集群管理 一、kafka 集群概述 1、kafka 集群概述&#xff1a; 集群是一种计算机系统&#xff0c;它通过一组松散集成的计算机软件和/或硬件连接起来高度紧密地协作完成计算工作。在某种意义上&#xff0c;他们可…

Webpack: 并行构建

概述 受限于 Node.js 的单线程架构&#xff0c;原生 Webpack 对所有资源文件做的所有解析、转译、合并操作本质上都是在同一个线程内串行执行&#xff0c;CPU 利用率极低&#xff0c;因此&#xff0c;理所当然地&#xff0c;社区出现了一些以多进程方式运行 Webpack&#xff0…

SQL Server详细使用教程(包含启动SQL server服务、建立数据库、建表的详细操作) 非常适合初学者

文章目录 目录 前言 一、启动SQL server服务的三种方法 1.不启动SQL server服务的影响 2.方法一&#xff1a;利用cmd启动SQL server服务 3.方法二&#xff1a;利用SQL Server配置管理器启动SQL server服务 4.方法三&#xff1a;在服务管理器中启动SQL server服务 二、建立数据库…

数据结构 —— 图的遍历

数据结构 —— 图的遍历 BFS&#xff08;广度遍历&#xff09;一道美团题DFS&#xff08;深度遍历&#xff09; 我们今天来看图的遍历&#xff0c;其实都是之前在二叉树中提过的方法&#xff0c;深度和广度遍历。 在这之前&#xff0c;我们先用一个邻接矩阵来表示一个图&#…

【Python新手入门指南】pip安装失败、下载慢、pip换源

文章目录 前言一、换源的基本命令是什么&#xff1f;二、如何从官方来换源总结 前言 对于Python新手而言&#xff0c;使用pip安装包就会成为一个问题&#xff0c;因为国内下载慢&#xff0c;甚至可能下载不成功&#xff0c;课程要安装库&#xff0c;但是连库都安装不成功&…

20240705 每日AI必读资讯

&#x1f4da;Retool 刚刚发布了最新2024上半年《人工智能现状报告》 - 收集了约750名技术人员的意见 - 包括开发者、数据团队和各行业的领导者&#xff0c;了解如何利用人工智能产生真正的影响。 &#x1f517; 2024上半年《人工智能现状报告》Retool刚刚发布了最新-CSDN b…

瑞数信息:智能防护新时代,看AI如何筑起网络防线

AI时代&#xff0c;网络安全危与机并行。 尤其是近年来大火的大模型&#xff0c;对于网络安全行业的影响与其他行业有所不同&#xff0c;一方面&#xff0c;AI能够通过大幅降低了安全攻击的门槛&#xff0c;网络威胁的复杂性和多样性不断增加&#xff0c;如自动化攻击、零日漏…