实现多选树形组件,我把递归用明白了

在一次项目中,需要定制一款树形组件,看了下UI组件中样式都不是太能满足,然后就自己实现了一个版本。感觉还是有不少知识点的,对于递归 的使用更熟练了,向上 递归查找,以及向下 递归查找。

效果如下:

相关功能支持:

    1. 数据结构设计
    1. vue递归组件实现
    1. 支持单选、多选
    • 3-1. 选中一个子元素,父元素需要半选 indeterminate = true
    • 3-2. 选中了所有子元素,父元素需要勾选 checked = true
    • 3-3. 选中了带子级的元素,需要把所有自己都勾选
    • 3-4. 取消选中同上

数据结构设计

算是一个典型的树形数据结构了如下

js 复制代码
const treeData = [
    {
    id: '1', // id 唯一
    name: 'tree-1', // 展示的名字
    parentId: 'a1', // 父元素id,我这里其实没有用到
    isParent: true, // 是否是父元素,用来判断是否要进行递归等
    checked: false, // 是否选中
    visible: false, // 是否展开子级

    children: [
      id: '1-1', 
      name: 'tree-1-1',
      parentId: 'a1', 
      isParent: false, 
      checked: false, 
      visible: false,
    ]
  },
]

其实isParent可以不用直接用children?.length去判断逻辑,但是有的场景在交互设计 上不会一个接口返回全量数据 ,会在每次点击 后去获取下一层接口 数据。

实现vue的递归组件

我分别用vue options组合式API 来实现下递归组件,以及**emit**

第一种options方法,name值必须需要定义如下:

vue 复制代码
<template>
  <div class="pro-tree-checkbox":key="uuid">
     <div v-for="(item, idx) in data" :key="idx">
      <div class="pro-tree-checkbox__item" @click="onClick(item, idx)">
        <!-- 文本 -->
        <VantCheckbox
          @click.stop
          shape="square"
          @update:modelValue="onCheckboxChange(item)"
          :modelValue="item.checked"
          :indeterminate="item.indeterminate"
        >
          {{ item.name }}
        </VantCheckbox>
       
       <!-- 使用 item.children.length 判断是不是父级,是父级需要有个展开收取的按钮 -->
        <span v-if="item.children && item.children.length > 0">
          <right-outlined v-if="!item.visible" />
          <down-outlined v-else />
        </span>
      </div>

       <!-- 这里就开始递归下一层 -->
      <div class="pro-tree-checkbox__children" v-if="item.children && item.children.length > 0" v-show="item.visible">
        <ProTreeCheckbox :data="item.children" @change="onChange" />
      </div>
    </div>
  </div>
</template>


<script>
export default {
  name: 'ProTreeCheckbox', // 必须定义
  ...
}
</script>

组合式API 使用defineOptions去定义name值

js 复制代码
import { defineOptions } from 'vue'
defineOptions({
  name: 'ProTreeCheckbox',
  inheritAttrs: false
})

接下来实现展开收起事件:

js 复制代码
const onClick = (item, idx) => {
  if (item.children && item.children.length > 0) {
    // eslint-disable-next-line vue/no-mutating-props
    props.data[idx] = {
      ...item,
      visible: !item.visible
    }
    uuid.value = Math.random()
  }
}

我是直接更改的props的值,这么会方便很多,不然就需要递归去传递emit事件,然后用id去查对应数据更改visible,请注意 我的uuid.value重新赋值了一个随机数 ,用来更新当前递归组件(<div class="pro-tree-checkbox":key="uuid">...</div>)。

接下来实现vue递归向上emit事件,请注意下面一段代码

vue 复制代码
<VantCheckbox
   @click.stop
   shape="square"
   @update:modelValue="onCheckboxChange(item)"
   :modelValue="item.checked"
   :indeterminate="item.indeterminate"
>
  {{ item.name }}
</VantCheckbox>

...

<ProTreeCheckbox :data="item.children" @change="onChange" />

onCheckboxChange是绑定给checkbox多选框的change事件:

js 复制代码
import { defineEmits } from 'vue'
const emit = defineEmits(['change'])

const onCheckboxChange = item => {
  emit('change', item)
}

接下来实现子组件递归传上来的事件接收@change="onChange"

js 复制代码
const onChange = item => {
  emit('change', item)
}

如果有三级树形数据,点击了最后一层触发事件应该是这样的

实现多选的相关逻辑

整体其实就是围绕着点击的元素进行数据更新,具体逻辑如下:

  • 选中:父级判断为全选、半选, 子级为全选
  • 取消选中:父级判断为全选、半选,子级为不选

咱们先实现俩个公用向下递归全选,全不选函数如下:

js 复制代码
// 全选
const checkedAllData = node => {
  node.indeterminate = false
  node.checked = true

  if (node?.children?.length) {
    for (const child of node.children) {
      checkedAllData(child)
    }
  }
}
// 全不选
const uncheckedAllData = node => {
  node.indeterminate = false
  node.checked = false

  if (node?.children?.length) {
    for (const child of node.children) {
      uncheckedAllData(child)
    }
  }
}

以上两个函数就是个递归处理数据全选、全不选 ,就不在多说了,下面会直接去使用。

接下来咱们实现向下查找 到当前点击的节点处理的方法,入参currentFunc 其实就是上面的全选、全不选函数

js 复制代码
// 向下查找 子级为全选、全不选
function downUpdataChecked(node, targetId, currentFunc) {
  if (node.id === targetId) { // 以ID 做对比
    currentFunc(node)
    return
  }

  if (node?.children?.length) {
    for (const child of node.children) {
      if (child.id === targetId) {
        currentFunc(child)
        break
      }

      if (child?.children?.length) {
        downUpdataChecked(child, targetId, currentFunc)
      }
    }
  }
}

以上就是查找到该节点,并更改状态

现在咱们写向上查找 ,更改父元素的全选、全不选、半选状态

js 复制代码
// 向上查找 更新 父级判断为全选、半选
function upUpdataChecked(node, targetId) {
  if (node.id === targetId) {
    return true
  }

  if (!node?.children) return false

  for (const child of node.children) {
    if (upUpdataChecked(child, targetId)) {
      const isChildrenCheckedAll = node.children.every(item => item.checked) // 是否全部选中
      const isChecked = node.children.some(item => item.checked || item.indeterminate) // 是否有选中

      if (isChildrenCheckedAll) {
        node.indeterminate = false
        node.checked = true
      } else if (isChecked) {
        node.indeterminate = true
        node.checked = false
      } else {
        node.indeterminate = false
        node.checked = false
      }
      return true
    }
  }
  return false
}

以上就是向上查找更改父元素的状态

使用以上两个方法downUpdataCheckedupUpdataChecked的代码如下:

js 复制代码
export const checkedHandle = (treeData, value) => {
  const isChecked = value.checked
  const currentFunc = isChecked ? uncheckedAllData : checkedAllData

  treeData.forEach(item => {
    downUpdataChecked(item, value.id, currentFunc)
    upUpdataChecked(item, value.id)
  })
}

ztree组件

ztree 需要引入JQ, 如果不是很介意引入jq,还是可以选择的。

总结

组件划分、函数处理数据,模块划分好,实现起来就省劲很多了。

本篇文章主要分享了:

    1. vue 递归组件的实现以及传值
    1. 递归处理相关数据

源码链接

相关推荐
moyu843 分钟前
高效开发必备:手把手整合IconFont、 Vant与Element Plus
前端·javascript
李帅朋3 分钟前
选择排序(简单选择排序、堆排序)
数据结构·算法·排序算法
trust Tomorrow4 分钟前
JS案例-基于Proxy的响应式数据
前端·javascript·css·html
BillKu6 分钟前
Vue3 + TypeScript,关于item[key]的报错处理方法
前端·javascript·vue.js·typescript
妄念鹿8 分钟前
关于tailwindcssV4版本官方插件没有提示
前端
七月丶9 分钟前
💬 打造丝滑交互体验:用 prompts 优化你的 CLI 工具(gix 实战)
前端·后端·github
愤怒的糖葫芦13 分钟前
异步编程进阶:Generator 与 Async/Await
前端·javascript
A丹18 分钟前
学习Vue2的贡献指南
vue.js
不想说话的麋鹿18 分钟前
「项目实战」从0搭建NestJS后端服务(八):静态资源访问以及文件上传
前端·node.js·全栈
liuxb18 分钟前
前端多标签主从管理方案分享
前端