基于vue3+element-plus的el-tree+select,实现获取最后一级数据,支持全选,全不选

2,845 阅读1分钟

目前用的是vue3+js,还没上ts。

用的递归实现数据过滤,但是数据量大的话,递归会比较耗性能,之前看到一个通过 map去把扁平数据变成树结构的数据,我在想能否也通过map()去实现数据过滤功能,但是暂时毫无头绪,请求指导。 正文开始:

需求: 1.要做一个人员树组件,会存在多个一级,或者一个一级,只需要获取最后一级的员工数据(这里用了递归)。 2.需要实现全选功能(我这里为了省力,直接在存在多个一级时,插入一个“全选”的0级节点,当然也写了全选的方法,就是没有直接加0级节点效果好)。 3.为了避免重名问题,需要获取部门名字。 效果图: 图片.png

操作演示:

el-tree-image.gif 数据过滤这里,直接用el-tree自带的:filter-node-method属性+watch就可以实现,只是过滤时需要设置一步:如果存在children,就禁选,保证只有最后一级可选。

<template>
<ElTree
    ref="treeRef"
    empty-text="暂无数据"
    :data="userList"
    ...
    :filter-node-method="filterNode"
/>
</template>
<script setup>

const filterNode = (value, data, node) => {
  if (!value) {
    data.disabled = false
    return true
  } else {
    //带过滤条件时,所有父级都禁止勾选
    // array.map(k => (!k.disabled) && (_childArr.push(k)))
    (data.children) && (data.disabled = true)
  }
  let _arr = []
  getParentNode(node, _arr, value)
  let result = false
  _arr.forEach(item => {
    result = result || item
  })
  return result
}
// tree数据过滤
watch(keyword, (newVal) => {
  treeRef.value.filter(newVal)
})
</script>

清空选中的方法:

// 清空CheckList
const clearCheckList = () => {
  if (!treeRef?.value) return
  treeRef.value.setCheckedKeys([])
  while (checkList.value.length > 0) {
    checkList.value.pop()
  }
}

全选 /全不选

const handleCheckAll = (array) => {
  treeRef.value.setCheckedNodes(array)
  array.map(item => {
    handleCheckList(item)
  })
}

在勾选时,通过递归实现获取最后一级的人员数据:

// 勾选或者取消勾选
const handleCheckList = (val) => {
  const _node = treeRef?.value?.getNode(val.id)
  if (!_node) return
  const _checkList = checkList.value
  if (_node.checked) { // 勾选时
    if (_node.checked && _node.childNodes.length === 0) {
      _checkList.push(_node)
    }
    // 通过new Set() 实现数据去重
    checkList.value = Array.from(new Set(_checkList))
    // 遍历子级,递归,直到获取最后一级的人员
    if (_node.childNodes.length > 0) {
      _node.childNodes.map(item => handleCheckList(item.data))
    }
  } else if (!_node.checked) { // 取消勾选
    checkList.value = checkList.value.filter(item => item.checked)
  }
}

全部代码:

<script setup name="MultiTreeSelector">
import { ref,  watch, onMounted } from 'vue'

const treeProps = {
  value: 'id',
  label: 'label',
  disabled: 'disabled',
  children: 'children'
}
const treeRef = ref()
const isExpandAll = ref(true) // 是否全展开
const keyword = ref('') // 搜索关键子
const checkList = ref([]) // 选中的list
const userList = ref([])
const resData = ref([
  {
    id: '3',
    label: '营销部',
    children: [
      {
        id: '3-1',
        label: '张三'
      },
      {
        id: '3-2',
        label: '张三'
      }
    ]
  },
  {
    id: '2',
    label: '业务部',
    children: [
      {
        id: '2-1-1',
        label: '业务部子部1',
        children: [
          {
            id: '2-1-0001',
            label: '张四'
          },
          {
            id: '2-1-0002',
            label: '从五'
          }
        ]
      },
      {
        id: '2-2-1',
        label: '业务部子部222',
        children: [
          {
            id: '2-2-00006',
            label: '业务四'
          },
          {
            id: '2-2-00005',
            label: '业务五'
          }
        ]
      }
    ]
  },
  {
    id: '4',
    label: '软件开发部',
    children: [
      {
        id: '4-1',
        label: '李四'
      },
      {
        id: '5-2',
        label: '王五'
      }
    ]
  }
])

// 勾选或者取消勾选
const handleCheckList = (val) => {
  const _node = treeRef?.value?.getNode(val.id)
  if (!_node) return
  const _checkList = checkList.value
  if (_node.checked) { // 勾选时
    if (_node.checked && _node.childNodes.length === 0) {
      _checkList.push(_node)
    }
    // 通过new Set() 实现数据去重
    checkList.value = Array.from(new Set(_checkList))
    // 遍历子级,递归,直到获取最后一级的人员
    if (_node.childNodes.length > 0) {
      _node.childNodes.map(item => handleCheckList(item.data))
    }
  } else if (!_node.checked) { // 取消勾选
    checkList.value = checkList.value.filter(item => item.checked)
  }
}

/* 人员树 父级过滤的递归
* @node 当前节点
* @result 存结果的数组
* @key
* */
const getParentNode = (node, result, key) => {
  let isPass = node?.data?.label?.indexOf(key) !== -1
  isPass ? result.push(isPass) : ''
  if (!isPass && node.level !== 1 && node.parent) {
    return getParentNode(node.parent, result, key)
  }
}
/*人员树过滤
* @value 过滤的关键字
* @data 被过滤的tree
* @node
* */
const filterNode = (value, data, node) => {
  if (!value) {
    data.disabled = false
    return true
  } else {
    //带过滤条件时,所有父级都禁止勾选
    // array.map(k => (!k.disabled) && (_childArr.push(k)))
    (data.children) && (data.disabled = true)
  }
  let _arr = []
  getParentNode(node, _arr, value)
  let result = false
  _arr.forEach(item => {
    result = result || item
  })
  return result
}
// 通过tag 取消勾选
const handleCloseTag = (tag) => {
  if (!treeRef?.value) return
  checkList.value = checkList.value.filter(item => {
    treeRef.value.setChecked(tag, false)
    if (!item?.data) return
    return item.data.id !== tag.data.id
  })
}

// 清空CheckList
const clearCheckList = () => {
  if (!treeRef?.value) return
  treeRef.value.setCheckedKeys([])
  while (checkList.value.length > 0) {
    checkList.value.pop()
  }
}

// 全选 /全不选
const handleCheckAll = (array) => {
  treeRef.value.setCheckedNodes(array)
  array.map(item => {
    handleCheckList(item)
  })
}

// 折叠/展开
const expandAll = () => {
  const nodesMap = treeRef.value.store.nodesMap
  for (let key in nodesMap) {
    nodesMap[key].expanded = isExpandAll.value
  }
}
const saveAsCommon = () => {
  console.log('保存为个人分组,功能待开发')
}
// 执行tree数据过滤
watch(keyword, (newVal) => {
  treeRef.value.filter(newVal)
})
onMounted(() => {
  // 全选方法2:插入 全选node
  userList.value = [{
    id: '全选',
    label: '全选',
    children: [...resData.value]
  }]
})
</script>
<template>
  <div class="multi-tree-selector flex oh">
    <div class="flex-1 flex-column">
      <div class="tree-toolbar flex">
        <ElInput class="search-input flex-1" v-model="keyword" placeholder="搜索">
          <template #prefix>
            <ElIcon class="el-input__icon">
              <Search/>
            </ElIcon>
          </template>
        </ElInput>
        <div class="tree-toolbar__right">
          <ElCheckbox v-model="isExpandAll" @change="expandAll">展开全部</ElCheckbox>
          <ElButton @click="handleCheckAll(userList)" size="small">全选</ElButton>
          <ElButton @click="handleCheckAll([] )" size="small">全不选</ElButton>
        </div>
      </div>
      <div class="flex-1 oh">
        <ElScrollbar height="100%">
          <ElTree
              ref="treeRef"
              node-key="id"
              empty-text="暂无数据"
              :data="userList"
              :props="treeProps"
              show-checkbox
              highlight-current
              :default-expand-all="isExpandAll"
              @check="handleCheckList"
              :filter-node-method="filterNode"
          />
        </ElScrollbar>
      </div>
    </div>
    <div class="multi-tree-selector__right">
      <div class="multi-tree-selector__right__header">
        <span class="mr-1">已选: {{ checkList.length }}</span>
        <ElButton type="primary" link @click="saveAsCommon">保存为个人分组</ElButton>
        <ElButton class="fr" link @click="clearCheckList">清空</ElButton>
      </div>
      <ElScrollbar class="multi-tree-selector__right__content" height="calc(100% - 49px)">
        <ElTag v-for="user in checkList" :key="user.id" @close="handleCloseTag(user)" closable>{{
            user.label
          }}[{{
            user.parent.data.label
          }}]
        </ElTag>
      </ElScrollbar>
    </div>
  </div>
</template>

<style lang="scss">
.multi-tree-selector {
  width: 680px;
  height: 400px;
  background: #fff;
  z-index: 999;

  .tree-toolbar {
    border-bottom: 1px solid var(--border-light);

    .tree-toolbar__right {
      padding-right: var(--space-primary);

      .el-checkbox {
        padding-top: 10px;
        margin-right: 8px;
        vertical-align: -4px;
      }

      .el-button {
        //margin-top: 10px;
      }
    }
  }

  .search-input {
    height: 49px;

    .el-input__wrapper {
      box-shadow: none;
    }
  }

  .multi-tree-selector__right {
    width: 40%;
    border-left: 1px solid var(--border-light);

    .multi-tree-selector__right__header {
      height: 49px;
      padding: var(--space-primary);
      border-bottom: 1px solid var(--border-light);
      box-sizing: border-box;
    }

    .multi-tree-selector__right__content {
      padding: 0 0 var(--space-primary) var(--space-primary);

      .el-tag {
        margin-top: var(--space-primary);
        margin-right: var(--space-primary);
      }
    }
  }

}

/*以下为 通用样式*/
.flex {
  display: flex;
}

.flex-column {
  display: flex;
  flex-direction: column;
}

.flex-1 {
  flex: 1;
}

.oh {
  overflow: hidden;
}
</style>