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

在一次项目中,需要定制一款树形组件,看了下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. 递归处理相关数据

源码链接

相关推荐
wearegogog1233 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars3 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤3 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·3 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°3 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
Irene19914 小时前
Vue3 <Suspense> 使用指南与注意事项
vue.js·suspense
qq_419854054 小时前
CSS动效
前端·javascript·css
烛阴4 小时前
3D字体TextGeometry
前端·webgl·three.js
短剑重铸之日4 小时前
《7天学会Redis》Day2 - 深入Redis数据结构与底层实现
数据结构·数据库·redis·后端
桜吹雪4 小时前
markstream-vue实战踩坑笔记
前端