RuoYi-Vue3树形结构实现:部门管理与菜单层级展示

RuoYi-Vue3树形结构实现:部门管理与菜单层级展示

【免费下载链接】RuoYi-Vue3 :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统 【免费下载链接】RuoYi-Vue3 项目地址: https://gitcode.com/GitHub_Trending/ruo/RuoYi-Vue3

引言:树形结构在权限系统中的核心价值

你是否还在为复杂组织架构的层级展示而困扰?是否在实现动态菜单时遭遇数据嵌套难题?RuoYi-Vue3作为基于SpringBoot和Vue3的前后端分离权限管理系统,通过优雅的树形结构实现,完美解决了部门层级管理与动态菜单展示的核心需求。本文将深入剖析RuoYi-Vue3中树形结构的设计理念、实现方式及最佳实践,带你掌握企业级应用中树形数据的处理精髓。

读完本文,你将获得:

  • 部门管理模块的树形数据渲染全流程解析
  • 动态菜单的递归构建与权限控制实现方案
  • Element Plus组件在树形结构中的高级应用技巧
  • 前后端协同处理树形数据的最佳实践指南

一、树形结构核心实现原理

1.1 数据结构设计:从扁平化到层级化

树形结构的本质是通过父子关系构建的数据模型。在RuoYi-Vue3中,部门和菜单均采用以下核心字段实现层级关联:

// 部门数据结构示例
{
  deptId: 1,             // 节点唯一标识
  parentId: 0,           // 父节点ID(0表示根节点)
  deptName: "技术部",     // 节点名称
  children: [            // 子节点集合
    {
      deptId: 2,
      parentId: 1,
      deptName: "前端团队",
      children: []
    }
  ]
}

// 菜单数据结构示例
{
  menuId: 1,             // 节点唯一标识
  parentId: 0,           // 父节点ID
  menuName: "系统管理",   // 节点名称
  icon: "system",        // 菜单图标
  path: "/system",       // 路由路径
  children: []           // 子节点集合
}

1.2 树形数据转换:handleTree工具函数

RuoYi-Vue3提供了handleTree工具函数,实现从扁平数组到层级结构的转换:

// 工具函数核心逻辑(简化版)
function handleTree(data, id = 'id', parentId = 'parentId', children = 'children') {
  const tree = []
  const map = {}
  
  // 构建ID到节点的映射
  data.forEach(item => {
    map[item[id]] = item
  })
  
  // 构建树形结构
  data.forEach(item => {
    const parent = map[item[parentId]]
    if (parent) {
      (parent[children] || (parent[children] = [])).push(item)
    } else {
      tree.push(item)
    }
  })
  
  return tree
}

// 应用示例
const flatData = [
  { deptId: 1, parentId: 0, deptName: "技术部" },
  { deptId: 2, parentId: 1, deptName: "前端团队" }
]

const treeData = handleTree(flatData, 'deptId', 'parentId', 'children')

1.3 树形组件配置:Element Plus的el-table与tree-props

RuoYi-Vue3采用Element Plus的el-table组件实现树形表格展示,核心配置如下:

<el-table
  :data="deptList"
  row-key="deptId"
  :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
  <!-- 表格列定义 -->
  <el-table-column prop="deptName" label="部门名称"></el-table-column>
  <el-table-column prop="status" label="状态"></el-table-column>
</el-table>

关键属性说明:

  • row-key:指定唯一标识字段,确保树形结构正确渲染
  • tree-props:配置树形属性映射
    • children:指定子节点数组字段名
    • hasChildren:指定是否有子节点的判断字段

二、部门管理模块树形实现详解

2.1 后端接口设计:数据获取与处理

部门管理模块提供了完整的CRUD接口,支持树形数据的查询与操作:

// src/api/system/dept.js
import request from '@/utils/request'

// 查询部门列表(树形结构)
export function listDept(query) {
  return request({
    url: '/system/dept/list',
    method: 'get',
    params: query
  })
}

// 查询部门列表(排除指定节点)
export function listDeptExcludeChild(deptId) {
  return request({
    url: '/system/dept/list/exclude/' + deptId,
    method: 'get'
  })
}

// 新增/修改/删除部门接口省略...

2.2 前端实现核心代码解析

部门管理页面(src/views/system/dept/index.vue)是树形结构实现的典型案例,其核心实现流程如下:

2.2.1 树形表格渲染
<el-table
  v-loading="loading"
  :data="deptList"
  row-key="deptId"
  :default-expand-all="isExpandAll"
  :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
  <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
  <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
  <el-table-column prop="status" label="状态" width="100">
    <template #default="scope">
      <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
    </template>
  </el-table-column>
  <!-- 操作列省略 -->
</el-table>
2.2.2 数据加载与树形转换
// 部门列表数据加载
function getList() {
  loading.value = true
  listDept(queryParams.value).then(response => {
    // 将后端返回的扁平数据转换为树形结构
    deptList.value = proxy.handleTree(response.data, "deptId")
    loading.value = false
  })
}
2.2.3 树形选择器:部门新增/编辑时的父节点选择
<el-dialog :title="title" v-model="open">
  <el-form ref="deptRef" :model="form">
    <el-form-item label="上级部门" prop="parentId">
      <el-tree-select
        v-model="form.parentId"
        :data="deptOptions"
        :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
        value-key="deptId"
        placeholder="选择上级部门"
        check-strictly
      />
    </el-form-item>
    <!-- 其他表单字段省略 -->
  </el-form>
</el-dialog>
// 加载部门树形选择器数据
function handleAdd(row) {
  reset()
  // 获取部门列表并转换为树形结构
  listDept().then(response => {
    deptOptions.value = proxy.handleTree(response.data, "deptId")
  })
  // 设置父部门ID(如果从子节点新增)
  if (row != undefined) {
    form.value.parentId = row.deptId
  }
  open.value = true
  title.value = "添加部门"
}

2.3 部门树形操作高级特性

2.3.1 全部展开/折叠功能
<el-button 
  type="info" 
  plain 
  icon="Sort" 
  @click="toggleExpandAll"
>展开/折叠</el-button>
// 切换展开/折叠状态
function toggleExpandAll() {
  refreshTable.value = false  // 强制表格重新渲染
  isExpandAll.value = !isExpandAll.value
  nextTick(() => {
    refreshTable.value = true
  })
}
2.3.2 部门新增的层级限制

在部门新增时,通过父节点ID的传递,实现当前节点下的子部门创建:

// 从列表中点击"新增"按钮时传递当前部门ID
<el-button 
  link 
  type="primary" 
  icon="Plus" 
  @click="handleAdd(scope.row)" 
  v-hasPermi="['system:dept:add']" 
>新增</el-button>

// 在handleAdd方法中接收并设置父部门ID
function handleAdd(row) {
  if (row != undefined) {
    form.value.parentId = row.deptId
  }
  // ...其他逻辑
}
2.3.3 删除时的子节点校验

删除部门时需要校验是否存在子部门,避免孤立节点产生:

function handleDelete(row) {
  proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
    return delDept(row.deptId)
  }).then(() => {
    getList()
    proxy.$modal.msgSuccess("删除成功")
  }).catch(() => {})
}

三、菜单管理树形实现

3.1 菜单与部门树形实现的异同点

特性部门树形菜单树形
核心标识deptIdmenuId
节点类型单一类型三种类型(目录M/菜单C/按钮F)
额外属性负责人、电话、邮箱图标、路由、权限标识、组件路径
交互功能展开/折叠、新增子部门图标选择、路由配置、权限控制
数据转换handleTree(deptList, 'deptId')handleTree(menuList, 'menuId')

3.2 菜单树形特有的实现要点

3.2.1 菜单类型的差异化处理

菜单模块支持三种节点类型,需要根据不同类型动态渲染表单:

<el-form-item label="菜单类型" prop="menuType">
  <el-radio-group v-model="form.menuType">
    <el-radio value="M">目录</el-radio>
    <el-radio value="C">菜单</el-radio>
    <el-radio value="F">按钮</el-radio>
  </el-radio-group>
</el-form-item>

<!-- 根据菜单类型动态显示不同表单字段 -->
<el-col :span="12" v-if="form.menuType == 'C'">
  <el-form-item prop="component">
    <template #label>
      <span>
        <el-tooltip content="访问的组件路径,如:`system/user/index`" placement="top">
          <el-icon><question-filled /></el-icon>
        </el-tooltip>
        组件路径
      </span>
    </template>
    <el-input v-model="form.component" placeholder="请输入组件路径" />
  </el-form-item>
</el-col>
3.2.2 菜单图标选择器实现

菜单模块集成了图标选择器,支持可视化图标选择:

<el-form-item label="菜单图标" prop="icon">
  <el-popover
    placement="bottom-start"
    :width="540"
    trigger="click"
  >
    <template #reference>
      <el-input v-model="form.icon" placeholder="点击选择图标" readonly>
        <template #prefix>
          <svg-icon v-if="form.icon" :icon-class="form.icon" />
        </template>
      </el-input>
    </template>
    <icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
  </el-popover>
</el-form-item>
// 选择图标回调
function selected(name) {
  form.value.icon = name
}
3.2.3 路由与组件的联动配置

菜单作为前端路由的直接映射,需要精确配置路由相关属性:

<el-col :span="12" v-if="form.menuType != 'F'">
  <el-form-item prop="path">
    <template #label>
      <span>
        <el-tooltip content="访问的路由地址,如:`user`" placement="top">
          <el-icon><question-filled /></el-icon>
        </el-tooltip>
        路由地址
      </span>
    </template>
    <el-input v-model="form.path" placeholder="请输入路由地址" />
  </el-form-item>
</el-col>

3.3 菜单树形与权限控制的集成

3.3.1 基于角色的菜单过滤

在用户登录后,后端根据用户角色权限返回可访问的菜单列表,前端直接渲染有权限的菜单:

// src/store/modules/permission.js
// 生成路由
export const generateRoutes = async () => {
  // 获取基于角色的菜单列表
  const menus = await getRouters()
  
  // 转换为路由格式
  const routes = filterAsyncRoutes(menus)
  
  // 动态添加路由
  routes.forEach(route => {
    router.addRoute(route)
  })
  
  return routes
}
3.3.2 按钮级权限控制

菜单中的"按钮"类型节点,通过权限标识控制页面元素的显示:

<el-button 
  link 
  type="primary" 
  icon="Edit" 
  @click="handleUpdate(scope.row)" 
  v-hasPermi="['system:menu:edit']"
>修改</el-button>

权限指令实现原理:

// src/directive/permission/hasPermi.js
export default {
  mounted(el, binding) {
    const { value } = binding
    const all_permission = "*:*:*"
    const permissions = store.getters && store.getters.permissions
    
    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value
      
      // 判断是否拥有权限
      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })
      
      // 无权限则移除元素
      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置操作权限标签值`)
    }
  }
}

四、树形结构高级应用与最佳实践

4.1 前后端协同处理树形数据

4.1.1 数据流转流程

mermaid

4.1.2 性能优化策略
  1. 懒加载实现:对于超大型树形结构(如十万级节点),采用懒加载策略:
<el-table
  :data="deptList"
  row-key="deptId"
  :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
  @expand-change="handleExpandChange"
>
  <!-- 表格列定义省略 -->
</el-table>
// 节点展开时加载子数据
function handleExpandChange(row, expanded) {
  if (expanded && row.hasChildren && !row.children.length) {
    // 加载子节点数据
    loadChildren(row.deptId).then(children => {
      row.children = children
    })
  }
}
  1. 数据缓存策略:缓存已加载的树形数据,避免重复请求:
// 使用localStorage缓存树形数据
const cacheKey = 'dept_tree_data'

function getDeptTree() {
  // 先从缓存获取
  const cachedData = localStorage.getItem(cacheKey)
  if (cachedData) {
    return Promise.resolve(JSON.parse(cachedData))
  }
  
  // 缓存不存在则请求接口
  return listDept().then(response => {
    const treeData = proxy.handleTree(response.data, "deptId")
    // 缓存数据(设置过期时间)
    localStorage.setItem(cacheKey, JSON.stringify(treeData))
    localStorage.setItem(cacheKey + '_expire', Date.now() + 3600 * 1000)
    return treeData
  })
}

4.2 树形结构常见问题解决方案

4.2.1 无限递归问题

当数据中存在循环引用(如A是B的父节点,B又是A的父节点)时,会导致树形转换进入死循环。解决方案:

// 增强版handleTree增加循环引用检测
function handleTree(data, id = 'id', parentId = 'parentId', children = 'children') {
  const tree = []
  const map = {}
  const visited = new Set()
  
  data.forEach(item => {
    map[item[id]] = item
  })
  
  data.forEach(item => {
    const itemId = item[id]
    if (visited.has(itemId)) return
    
    let current = item
    const path = []
    
    // 检测循环引用
    while (current) {
      const currentId = current[id]
      if (path.includes(currentId)) {
        console.error('检测到循环引用:', path)
        return  // 跳过循环引用节点
      }
      
      path.push(currentId)
      visited.add(currentId)
      
      const pid = current[parentId]
      current = map[pid]
    }
    
    // 正常构建树形结构
    const parent = map[item[parentId]]
    if (parent) {
      (parent[children] || (parent[children] = [])).push(item)
    } else {
      tree.push(item)
    }
  })
  
  return tree
}
4.2.2 节点排序问题

确保树形结构中的节点按预设顺序显示:

// 树形转换时增加排序
function handleTree(data, id = 'id', parentId = 'parentId', children = 'children', sortKey = 'orderNum') {
  // 基础树形转换逻辑省略...
  
  // 递归排序子节点
  function sortChildren(node) {
    if (node[children] && node[children].length > 0) {
      // 按排序字段升序排列
      node[children].sort((a, b) => a[sortKey] - b[sortKey])
      // 递归排序子节点
      node[children].forEach(child => sortChildren(child))
    }
  }
  
  // 对根节点排序
  tree.sort((a, b) => a[sortKey] - b[sortKey])
  // 对子节点排序
  tree.forEach(node => sortChildren(node))
  
  return tree
}

五、总结与展望

5.1 核心知识点回顾

RuoYi-Vue3中的树形结构实现基于以下关键技术点:

  1. 数据转换:通过handleTree工具函数将扁平数据转换为层级结构
  2. 组件应用:Element Plus的el-table和el-tree-select组件的组合使用
  3. 递归思想:父子节点的递归处理实现无限层级展示
  4. 权限控制:基于角色的菜单过滤与按钮级权限控制

5.2 进阶应用场景拓展

  1. 树形拖拽排序:结合SortableJS实现节点拖拽调整顺序
  2. 树形复选功能:实现批量选择与级联选择
  3. 可视化树形编辑:通过图形化界面直接编辑树形结构
  4. 树形数据导出:支持将树形结构导出为Excel或JSON

5.3 最佳实践 checklist

  •  始终为树形组件指定row-key,确保节点唯一性
  •  使用handleTree统一处理树形数据转换
  •  对大型树形结构实现懒加载优化
  •  为节点操作添加权限控制
  •  实现树形数据的缓存策略
  •  检测并处理循环引用问题

通过本文的深入剖析,相信你已经掌握了RuoYi-Vue3中树形结构的核心实现原理与最佳实践。树形结构作为权限管理系统的基础组件,其设计质量直接影响整个系统的用户体验与性能表现。希望本文提供的技术方案能帮助你在实际项目中构建更高效、更灵活的树形结构应用。

【免费下载链接】RuoYi-Vue3 :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统 【免费下载链接】RuoYi-Vue3 项目地址: https://gitcode.com/GitHub_Trending/ruo/RuoYi-Vue3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值