拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vue2组织架构图组件vue2-org-tree实战:从数据渲染到动态加载

1. 组织架构图在Vue2项目里的真实需求场景很多后台管理系统做到一定阶段都会碰到一个绕不开的需求把公司、部门、岗位的层级关系可视化出来。最典型的就是组织架构图从集团往下拆到事业部、中心、部门、小组甚至具体到人。这个东西看起来简单真做起来坑不少。我接过一个内部管理系统的活需求方一开始说得很轻松“就画个树状图能展开收起就行。”结果细聊才发现他们要的不只是静态展示还要支持点击节点查看详情、动态从接口拉取子节点、节点上挂不同颜色的状态标签、横向纵向都能切换布局。这时候如果自己用div加CSS硬画光是节点连线的对齐问题就能耗掉两三天。vue2-org-tree这个组件就是在这种场景下进入视野的。它是一个专门为Vue2生态设计的组织架构树组件基于树形数据结构渲染支持横向和纵向两种布局节点内容可以通过插槽完全自定义展开收起、点击事件这些交互也都内置了。说白了它帮你把“树形结构的布局计算”和“节点连线的绘制”这两件最烦的事做掉了你只需要关心数据怎么来、节点长什么样。这篇文章适合两类人看一类是正在做Vue2后台项目、需要快速落地组织架构图的前端开发者另一类是想搞清楚这类树形组件底层是怎么运作的、以后遇到类似需求能自己改造的人。我会从数据结构的组织方式讲起到组件的引入配置、动态加载子节点的实现、自定义节点样式的技巧再到实际项目中踩过的坑尽量把每个环节的“为什么”说清楚。需要提前说明的是vue2-org-tree是一个相对轻量的组件它的定位是“把树画好”而不是“把树画成流程图”。如果你需要的是那种带贝塞尔曲线连线、节点可以自由拖拽的复杂拓扑图那它可能不是最优解。但对于绝大多数组织架构展示场景它足够用而且上手成本低。2. 数据结构设计与组件渲染的底层逻辑2.1 树形数据的字段约定与扩展思路vue2-org-tree对数据格式的要求很明确一个根节点对象通过children字段嵌套子节点。最小结构长这样const treeData { id: 1, label: 总公司, children: [ { id: 2, label: 技术中心, children: [ { id: 4, label: 前端组 }, { id: 5, label: 后端组 } ] }, { id: 3, label: 市场中心, children: [ { id: 6, label: 华北区 }, { id: 7, label: 华南区 } ] } ] }id和label是组件默认读取的字段id用于标识节点label用于显示文本。但实际项目里节点上要展示的信息远不止一个名字。你可能需要显示头像、职位、人数、状态标签等等。这时候有两种做法第一种是直接在节点对象上挂自定义字段然后通过组件的render-content属性或者labelClassName来定制渲染。第二种是保持数据结构干净把展示逻辑全部放到插槽里。我更推荐第二种原因后面讲自定义节点时会展开说。这里有个容易被忽略的点children字段名是可以改的。组件提供了props配置项你可以把children映射成subNodes或者别的名字。这个在对接后端接口时特别有用因为后端返回的字段名往往不叫children可能是subList、childNodes之类的。与其在前端做一次数据转换不如直接配置映射关系少一层处理少一层出错的可能。// 组件配置 { props: { children: subList, label: name, expand: expanded } }上面这段配置的意思是组件读取子节点时找subList字段显示文本时找name字段判断是否展开时找expanded字段。这样后端返回什么结构前端配一下就能直接用不用写递归转换函数。2.2 组件渲染一棵树的完整过程理解组件的渲染逻辑对排查问题和做性能优化很有帮助。vue2-org-tree的内部实现大致是这样的组件接收根节点数据后会先做一次递归遍历把整棵树拍平成一个节点数组同时计算每个节点的层级、父节点引用、在兄弟节点中的位置等信息。然后根据布局方向横向或纵向计算每个节点的坐标位置。纵向布局时同一层的节点水平排列父节点在子节点上方居中横向布局时层级从左到右展开同一父节点下的子节点垂直排列。节点之间的连线不是用图片或SVG画的而是用CSS的伪元素加边框实现的。每个节点容器上会有对应的class标记它是父节点还是子节点、是第一个还是最后一个CSS根据这些class决定边框怎么画。这种做法的好处是连线能自适应节点大小变化缺点是当节点内容高度不一致时连线可能出现细微的错位。展开收起功能是通过控制节点的expand状态实现的。组件内部维护了一个展开状态的映射表点击节点时切换对应节点的状态然后重新计算可见节点列表并触发重新渲染。这里要注意展开收起不会重新请求数据它只是控制显示隐藏。如果你需要点击时才加载子节点数据得自己监听点击事件去请求接口拿到数据后更新到对应节点的children上。提示组件的expand状态默认是全部展开的。如果树很大建议初始只展开根节点和第一层子节点数据按需加载否则一次性渲染几百个节点会有明显的卡顿。2.3 横向与纵向布局的切换代价组件支持通过horizontal属性切换布局方向传true是横向默认是纵向。这个切换在代码层面只是改一个prop的事但实际使用时有几个细节要注意。纵向布局适合层级不深但每层节点多的场景比如一个公司下面有十几个部门每个部门下面几个人纵向展示一目了然。横向布局适合层级深但每层节点少的场景比如从集团到事业部到中心到部门到小组五六层下来横向展开比纵向更省空间。切换布局后节点的宽高计算会变如果你在节点上用了固定宽度的样式横向布局时可能会因为宽度不够导致文字换行错乱。我的做法是给节点容器设一个min-width而不是固定width让它可以自适应内容。另外横向布局时节点之间的水平间距是通过margin控制的如果觉得太挤可以调整组件外层的padding或者节点自身的margin。3. 从零接入vue2-org-tree的完整操作链路3.1 安装与全局注册的取舍安装很简单npm或者yarn都行npm install vue2-org-tree --save或者yarn add vue2-org-tree接下来是注册。你可以全局注册也可以在单个组件里局部注册。全局注册的好处是多个页面都能用不用重复引入坏处是如果项目里只有一两个页面用到会增加打包体积。我的建议是如果项目里超过三个页面需要组织架构图就全局注册否则局部注册更合适。全局注册的写法import Vue from vue import Vue2OrgTree from vue2-org-tree import vue2-org-tree/dist/style.css Vue.use(Vue2OrgTree)局部注册的写法import Vue2OrgTree from vue2-org-tree import vue2-org-tree/dist/style.css export default { components: { Vue2OrgTree } }注意那个CSS文件必须引入否则节点连线和布局样式全都没有渲染出来就是一堆堆叠的文字。这个坑我见过不止一个新手踩组件功能正常但样式全无排查半天以为是版本问题其实就是少了一行import。3.2 最简可用示例与参数说明注册完之后在模板里用起来template div classorg-tree-wrapper vue2-org-tree :datatreeData :horizontalhorizontal :collapsablecollapsable :render-contentrenderContent on-node-clickhandleNodeClick / /div /template script export default { data() { return { horizontal: false, collapsable: true, treeData: { id: 1, label: 总公司, children: [ { id: 2, label: 技术中心, children: [ { id: 4, label: 前端组 }, { id: 5, label: 后端组 } ] }, { id: 3, label: 市场中心, children: [ { id: 6, label: 华北区 }, { id: 7, label: 华南区 } ] } ] } } }, methods: { handleNodeClick(e, data) { console.log(点击了节点, data) }, renderContent(h, data) { return h(div, { class: custom-node }, [ h(span, { class: node-label }, data.label), h(span, { class: node-count }, (${data.children ? data.children.length : 0}人)) ]) } } } /script style scoped .org-tree-wrapper { padding: 20px; overflow: auto; } .custom-node { display: flex; align-items: center; justify-content: center; padding: 8px 12px; } .node-label { font-weight: 500; } .node-count { margin-left: 6px; color: #999; font-size: 12px; } /style几个关键参数解释一下data树形数据必传。horizontal布局方向true横向false纵向默认false。collapsable是否允许点击节点收起展开默认true。render-content自定义节点渲染函数接收h和节点数据两个参数返回VNode。on-node-click节点点击事件回调参数是事件对象和节点数据。render-content这个属性是组件最核心的扩展点。它让你完全控制节点内部长什么样但要注意它用的是Vue的render函数写法不是模板语法。如果你不熟悉render函数可以先用默认的label展示等熟悉了再换成自定义渲染。3.3 动态加载子节点的实现方案实际项目里组织架构数据往往不是一次性返回的。可能是数据量太大也可能是权限控制导致不同用户看到的层级不同。这时候就需要点击节点时才去请求子节点数据。实现思路是初始只加载根节点和第一层每个节点的children字段先不设置或者设为空数组。监听on-node-click事件当点击的节点没有children或者children为空时调接口获取子节点数据然后通过this.$set或者直接赋值的方式更新到节点上。methods: { handleNodeClick(e, data) { if (!data.children || data.children.length 0) { this.loadChildren(data) } }, async loadChildren(node) { try { const res await this.$http.get(/api/org/children, { params: { parentId: node.id } }) const children res.data || [] if (children.length 0) { this.$set(node, children, []) this.$message.info(该节点下暂无子部门) return } this.$set(node, children, children) } catch (err) { this.$message.error(加载子节点失败) } } }这里用this.$set而不是直接node.children children是因为Vue2的响应式系统对新增属性不敏感。如果节点对象上原本没有children字段直接赋值不会触发视图更新。用$set才能保证新加的children是响应式的。还有一个细节加载子节点时最好加个loading状态避免用户重复点击。可以在节点数据上挂一个loading字段渲染时根据这个字段显示加载中的样式。注意动态加载时如果接口返回的数据结构里子节点字段名不是children记得在组件配置里做好映射或者在拿到数据后手动转换一次。4. 自定义节点样式与交互的进阶玩法4.1 用render-content打造信息密度更高的节点默认的节点就是一个带边框的矩形里面显示label文本。但组织架构图里节点往往需要承载更多信息部门名称、负责人、人数、状态标签等。用render-content可以完全自定义。renderContent(h, data) { const statusClass data.status active ? status-active : status-inactive return h(div, { class: [org-node, statusClass] }, [ h(div, { class: org-node-header }, [ h(span, { class: org-node-name }, data.label), h(span, { class: org-node-badge }, data.count ? ${data.count}人 : ) ]), data.manager ? h(div, { class: org-node-manager }, 负责人${data.manager}) : null ]) }对应的CSS.org-node { padding: 10px 16px; border-radius: 6px; background: #fff; border: 1px solid #e0e0e0; min-width: 120px; text-align: center; transition: box-shadow 0.2s; } .org-node:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); } .org-node-header { display: flex; align-items: center; justify-content: center; gap: 6px; } .org-node-name { font-size: 14px; font-weight: 500; color: #333; } .org-node-badge { font-size: 12px; color: #fff; background: #409eff; border-radius: 10px; padding: 1px 6px; } .org-node-manager { margin-top: 4px; font-size: 12px; color: #999; } .status-active { border-color: #67c23a; } .status-inactive { border-color: #dcdfe6; opacity: 0.7; }这样每个节点就能显示部门名、人数徽标和负责人信息信息密度比默认样式高很多而且视觉上更有层次。4.2 节点点击与展开收起的冲突处理组件默认点击节点会触发展开收起同时也会触发on-node-click事件。如果你希望点击节点只做业务操作比如打开详情弹窗不触发展开收起有两种做法第一种是把collapsable设为false这样点击节点不会收起展开但你也失去了展开收起的能力。如果树层级很深全部展开会很长这个方案就不合适。第二种是在节点渲染时把展开收起的触发区域限制在一个小图标上而不是整个节点。具体做法是在render-content里自己画一个展开收起按钮点击按钮时手动调用组件的展开收起方法点击节点其他区域时只触发业务逻辑。不过vue2-org-tree没有直接暴露展开收起的API方法所以第二种做法需要绕一下你可以通过修改节点的expand字段来控制展开状态。组件内部会读取节点的expand属性来决定是否展开子节点。renderContent(h, data) { const hasChildren data.children data.children.length 0 return h(div, { class: org-node }, [ h(span, { class: org-node-label }, data.label), hasChildren ? h(span, { class: [toggle-btn, data.expand ? expanded : collapsed], on: { click: (e) { e.stopPropagation() this.$set(data, expand, !data.expand) } } }, data.expand ? - : ) : null ]) }这样点击加减号图标才展开收起点击节点其他区域只触发on-node-click。注意要e.stopPropagation()阻止事件冒泡否则还是会触发组件的默认展开收起行为。4.3 大数据量下的性能优化手段当组织架构有几百个节点时一次性全部渲染会有明显的性能问题。除了前面说的按需加载子节点还有几个优化点第一初始只展开根节点。把根节点的expand设为true其他节点的expand设为false。这样初始只渲染根节点和第一层子节点数量可控。第二避免在render-content里做复杂计算。render-content会在每次渲染时对每个节点调用一次如果里面做了递归遍历或者复杂的数据处理节点多了之后会非常卡。把能提前算好的数据提前算好挂在节点对象上渲染时直接读取。第三如果节点内容高度差异很大考虑给节点设一个固定的min-height减少布局计算的开销。第四横向布局在节点多时比纵向布局更耗性能因为横向布局的连线计算更复杂。如果节点超过两百个建议用纵向布局。5. 实际项目中踩过的坑与排查过程5.1 节点不显示或连线错位的排查链路有一次在项目里接入组件后数据明明有但页面上只显示了根节点子节点一个都看不到。排查过程是这样的先检查数据格式确认children字段存在且是数组没问题。然后检查组件注册确认Vue.use和CSS引入都做了也没问题。接着打开浏览器开发者工具看DOM结构发现子节点的DOM其实渲染出来了但被父容器的overflow: hidden裁掉了。原来是我在外层容器上设了固定高度和overflow: hidden而组织架构图展开后高度超出了容器导致子节点被裁掉看不见。把overflow改成auto或者去掉固定高度就好了。连线错位的问题通常是节点内容高度不一致导致的。组件的连线是基于节点容器的边框计算的如果某个节点的内容比其他节点高很多连线就会对不齐。解决办法是给所有节点设一个统一的min-height或者用flex布局让同一层的节点高度一致。5.2 动态更新数据后视图不刷新的原因前面提到过Vue2对新增属性不响应。但还有一种情况你直接修改了节点对象的某个已有属性视图也没更新。比如把node.label改了但页面上还是旧文本。这是因为vue2-org-tree内部对节点数据做了缓存或者浅拷贝你修改原始数据后组件没有感知到变化。解决办法是修改完数据后手动触发一次重新渲染。可以通过给组件加一个key修改数据后更新key值来强制重新渲染vue2-org-tree :keytreeKey :datatreeData /this.treeData newData this.treeKey Date.now()这种做法简单粗暴但有效缺点是会丢失展开收起的状态。如果展开状态很重要就需要在更新数据前把展开状态保存下来更新后再恢复。5.3 样式覆盖不生效的优先级问题组件的默认样式是通过class加在节点上的比如.org-tree-node-label。你在scoped样式里写.org-tree-node-label { color: red }发现不生效。原因是scoped会给选择器加上属性选择器而组件内部的DOM是动态生成的属性选择器匹配不上。解决办法有两个一是用::v-deep或者/deep/穿透scoped::v-deep .org-tree-node-label { color: red; }二是把样式写在全局样式文件里不加scoped。但全局样式容易污染其他组件所以更推荐第一种。还有一个细节如果你用了render-content自定义节点节点内部的样式是你自己控制的不受组件默认样式影响这部分样式可以正常写在scoped里。6. 组织架构图组件的选型对比与扩展思路6.1 vue2-org-tree与其他方案的横向对比在Vue2生态里能画组织架构图的方案不止vue2-org-tree一个。我整理了一个对比表格方便你根据项目情况做选择方案布局能力自定义程度学习成本适用场景vue2-org-tree横向/纵向树形高支持render-content低标准组织架构展示ECharts tree树形图支持缩放拖拽中配置项驱动中需要缩放、导出图片D3.js完全自由极高需自己实现高复杂拓扑、定制连线手写递归组件完全自由极高中需求简单、不想引依赖vue2-org-tree的优势在于开箱即用API简单适合快速落地。ECharts的tree图表适合需要交互缩放和导出功能的场景但节点样式定制没有vue2-org-tree灵活。D3.js适合对连线样式有极致要求的场景但开发成本高。手写递归组件适合需求极简、不想引入额外依赖的项目。6.2 从组织架构图延伸到其他树形场景vue2-org-tree虽然叫组织架构图但它的本质是一个树形结构可视化组件。除了组织架构还可以用在很多类似场景产品分类树电商后台的商品类目管理多级分类展示。权限菜单树角色权限配置勾选不同层级的菜单权限。文件目录树网盘或文档管理系统的文件夹层级展示。工艺流程树制造业的工序分解从总装到零件。这些场景的数据结构都是树形的展示需求也类似用vue2-org-tree都能覆盖。区别在于节点的交互逻辑不同比如权限树需要勾选框文件树需要右键菜单。这些都可以通过render-content来实现。6.3 后续可以做的功能增强如果项目对组织架构图的要求比较高可以在vue2-org-tree基础上做这些增强第一加搜索定位功能。在树上方放一个搜索框输入关键词后高亮匹配的节点并自动展开到该节点。实现思路是遍历树找到匹配节点把从根节点到该节点的路径上所有节点的expand设为true然后滚动到该节点位置。第二加节点右键菜单。在render-content里监听contextmenu事件弹出自定义菜单支持新增子节点、编辑、删除等操作。第三加导出图片功能。这个需要借助html2canvas之类的库把组织架构图的DOM区域截取成图片下载。注意导出前要确保所有节点都是展开状态否则导出的图片不完整。第四加缩放和平移。组织架构图大了之后一屏显示不下需要支持鼠标滚轮缩放和拖拽平移。这个可以通过在外层容器上监听wheel和mousedown事件动态调整transform: scale()和translate()来实现。这些增强功能不需要改动组件源码都是在外层包装实现的保持了组件的纯粹性也方便后续升级。提示做缩放平移时要注意缩放后节点的点击事件坐标会偏移需要做坐标转换。如果不想处理这个复杂度可以用CSS的zoom属性代替transform: scale()zoom不会影响事件坐标但兼容性稍差。7. 一些零散但实用的经验记录组件的labelClassName属性可以给节点文本加额外的class用来做条件样式。比如不同状态的部门显示不同颜色就可以根据节点数据动态返回class名。如果组织架构图需要打印记得在打印样式里把overflow设为visible并且把横向布局改成纵向否则打印出来会被截断。vue2-org-tree的版本更新不算频繁但每次升级前建议先看changelog确认没有破坏性变更。我遇到过升级后render-content的参数顺序变了的情况虽然概率很低但升级前备份一下总没错。最后说一个数据层面的经验组织架构数据最好在后端做好排序前端不要依赖默认顺序。因为不同部门在不同层级的排序规则可能不一样有的按人数排有的按成立时间排这些逻辑放在后端更合适前端拿到什么顺序就渲染什么顺序简单可控。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门