ElementUI:el-tree和el-select树形多选搜索功能封装-vue2

3,165 阅读5分钟

一、需求描述

vue2,基于elementUI,封装el-tree和el-select组件,形成公司组织架构树,部门-人员树。支持多选、树形结构搜索等功能;

image.png

二、数据结构

整体上是一个长度为1的[{}]对象数组,内部包含了children、id、label(公司/部门名)、type。children也是对象数组,如果children下包含的是人员,则有departId、id、label(人名)、type(user)、userDisplayName(人中文名字)、userName(人英文名字)字段,如果children下包含的是部门,则有children、id、label(部门名)、type(depart)字段。层层嵌套~

结构示例

第一层:公司名

第二层:一级部门领导、二级部门1、二级部门2(children展开示例)

第三层:三级部门1、三级部门2、三级部门3、三级部门4(children展开示例)、三级部门5

第四层:四级部门1、2、3领导数据、四级部门1、四级部门2、四级部门3(children展开示例)

[{
  children:[
    {...},{
      children:[{
        departId:xxx,
        id:'xxx_user',
        label:'人名',// 一级部门领导
        type:'user',
        userDisplayName:'人名-中文名',
        userName:'人名-英文名'
      },
      {
        children:[{...}], // 省略二级部门名1的children的展示,具体看二级部门名2
        id:'111_depart',
        label:'二级部门名1',
        type:'depart'
      },
      {
        children:[{
          children:[{...}],// 省略三级部门名1的chilren展示,具体看三级部门名4
          id:'xxx_depart',
          label:'三级部门名1',
          type:'depart'
        },
        {
          children:[{...}],// 省略
          id:'xxx_depart',
          label:'三级部门名2',
          type:'depart'
        },
        {
          children:[{...}],// 省略
          id:'xxx_depart',
          label:'三级部门名3',
          type:'depart'
        },
        {
          children:[{
            departId:xxxx,
            id:'xxx_user',
            label:'人名',// 四级部门1、2、3领导
            type:'user',
            userDisplayName:'人名-中文名',
            userName:'人名-英文名'
          },
          {...},// 四级部门1
          {...},// 四级部门2
          {
            children:[{
              departId:@@@@,
              id:'xxxx_user',
              label:'人名',// 四级部门成员
              type:'user',
              userDisplayName:'人名-中文名',
              userName:'人名-英文名'
            },{
              departId:@@@@,
              id:'xxxx_user',
              label:'人名',// 四级部门成员
              type:'user',
              userDisplayName:'人名-中文名',
              userName:'人名-英文名'
            }],
            id:'@@@@_depart',
            label:'四级部门名3', // 四级部门3及其children数据
            type:'depart'}
          }],
          id:'xxx_depart',
          label:'三级部门名4',
          type:'depart'
        },
        {   
          children:[{...}],// 省略
          id:'xxx_depart',
          label:'三级部门名5',
          type:'depart'
         }],
        id:'2222_depart',
        label:'二级部门名2',
        type:'depart'
      }]
      id:'xxx_depart',
      label:'一级部门名',
      type:'depart'
    },...,{...}
  ],
  id:'1_depart',
  label:'公司名',
  type:'depart''
}]

三、实现

3.1 实现思路

step1:基础功能搭建

先搭建好el-select和el-tree结构,然后通过getTreeData()获取树形结构数据,然后通过触发el-tree的@check事件,将用户选择的树形结构数据传给父组件,结合vue2的父子数据传递方式props$emit给传给子组件作为:value的属性赋值,可以实现el-select+el-tree的选中和回显的效果;

step2:进一步完善:

1.el-select添加filter-method搜索功能,结合el-tree提供的filter方法,完成搜索功能; 2.在el-select清除选中的时候,要通过@change方法清除el-tree对应的数据,将变化emit给父组件,更新:value的值; 3.通过el-tree的node-click事件让人员添加功能支持点击复选框外的文字也生效;

step3:优化:

通过el-select的focus触发焦点事件,改造树形控件展示结构;

3.2 解释

Props

  • 接收来自父组件的属性如 placeholderdisabledvaluecollapseTags, 和 clearable,这些用于控制组件的行为和样式

Data

  • treeData: 存储树形结构数据。
  • defaultTreeProps: 配置 el-tree 的属性映射。
  • defaultKeys: 默认展开的树节点键值。
  • initTreeSelect: 标记是否初始化过树选择数据。
  • isFocus: 标记 el-select 是否处于聚焦状态。

Methods

  • filterTreeNode: 过滤树节点的方法,用于在树中搜索。
  • selectorFilter: 控制 el-select 的过滤行为,只有当 el-select 聚焦时才进行过滤。
  • getTreeData: 从服务器获取树形结构数据,并处理初始数据的回显。
  • onNodeClick: 当树中的节点被点击并选中时触发,处理选中节点的数据。
  • searchData: 当选择数据发生变化时触发,更新树中选中节点的状态。
  • setTreeCheckedNodes: 设置树中选中节点的方法。
  • onBlur: 当 el-select 失去焦点时触发。
  • onFocus: 当 el-select 获得焦点时触发,同时进行一些初始化操作。
  • handleNodeClick: 当树中的节点被点击时触发,处理节点的选中状态。
  • findUsers: 递归搜索树中的用户节点,用于回显数据。
  • transformData: 根据 URL 中携带的值来刷新和回显树中的选中状态。

四、代码

3.1 子组件

<template>
  <div>
    <el-select
      ref="userSelector"
      :value="value"
      value-key="id"
      filterable
      :multiple="true"
      :disabled="disabled"
      :filter-method="selectorFilter"
      :placeholder="placeholder"
      :clearable="clearable"
      :collapse-tags="collapseTags"
      style="width: 100%"
      popper-class="user-depart-selector-popper"
      @change="searchData"
      @focus="onFocus"
      @blur.capture.native="onBlur"
    >
      <el-option
        style="width: 100%; height: auto; max-height: available; overflow: auto; background-color: #fff"
        label="adminName"
        value="adminName"
        disabled
      >
        <el-tree
          ref="projectTree"
          :data="treeData"
          :props="defaultTreeProps"
          node-key="id"
          :default-expanded-keys="defaultKeys"
          :filter-node-method="filterTreeNode"
          show-checkbox
          @check="onNodeClick"
          accordion
          @node-click="handleNodeClick"
        ></el-tree>
      </el-option>
      <!-- 用于select展示选择数据用的-没有这部分代码el-select不会展示任何内容 -->
      <el-option
        v-show="false"
        v-for="item in value"
        :key="item.id"
        style="width: 100%; height: auto; max-height: available; overflow: auto; background-color: #fff"
        :label="item.userDisplayName"
        :value="item"
      ></el-option>
    </el-select>
  </div>
</template>

<script>
import { CommonService } from '@/api/api';

export default {
  name: 'userTreeSelectMutiple',
  props: {
    placeholder: {
      default: '人员'
    },
    disabled: {
      type: Boolean,
      default: false
    },
    value: {
      type: Array,
      default: () => {
        return [];
      }
    },
    // 多选时是否将选中值按文字的形式展示
    collapseTags: {
      type: Boolean,
      default: true
    },
    clearable: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      treeData: null,
      defaultTreeProps: {
        children: 'children',
        label: 'label'
      },
      defaultKeys: ['1_depart'], // 默认展开的部门-公司名展开-与顶层数据的id对应
      initTreeSelect: false, // 初始化树选择数据
      isFocus: false
    };
  },
  created() {
    this.getTreeData();
  },
  mounted() {},
  methods: {
    filterTreeNode(value, data) {
      if (!value) return true;
      if (data.userName == undefined) {
        return data.label.indexOf(value) !== -1;
      } else {
        return data.userName.indexOf(value) !== -1 || data.label.indexOf(value) !== -1;
      }
    },
    selectorFilter(value) {
      if (this.isFocus) {
        this.$refs.projectTree.filter(value);
      }
    },

    getTreeData() {
      CommonService.getAllDepartmentUsers().then((res) => {
        if (res.status == 200) {
          if (res.data.success) {
            this.treeData = res.data.data;
          } else {
            this.$message({ message: res.data.message, type: 'error' });
          }
        } else {
          this.$message({ message: '服务端请求异常', type: 'error' });
        }
      });
    },

    onNodeClick(item, { checkedNodes = [] }) {
      // 过滤出部门节点-否则点击部门也会选中部门名
      const filteredNodes = checkedNodes.filter((node) => node.type !== 'depart');
      const list = filteredNodes.map((item) => {
        return {
          id: item.id,
          userType: item.type,
          userDisplayName: item.label,
          userName: item.userName
        };
      });
      this.$emit('change', list); // 把用户勾选的情况给父组件,让父组件去更新select的值进行填充
    },
    // 搜索清除的时候触发
    searchData(data) {
      const checkedKeys = data.map((item) => {
        return {
          id: item.id,
          label: item.userDisplayName
        };
      });
      this.$refs.projectTree.setCheckedNodes(checkedKeys);
      this.$emit('change', data);
    },
    setTreeCheckedNodes(data) {
      const checkedKeys = data.map((item) => {
        return {
          id: item.id,
          label: item.userDisplayName
        };
      });
      this.$refs.projectTree.setCheckedNodes(checkedKeys);
    },
    onBlur() {
      this.isFocus = false;
    },
    onFocus() {
      this.isFocus = true;
      if (!this.initTreeSelect) {
        this.setTreeCheckedNodes(this.value);
      }
      this.$refs.projectTree.filter();
      // 因为搜索以后,树状结构只会保留筛选以后的数据,所以需要定位到当前人,展开当前人的所有父节点
      const nodes = this.$refs.projectTree.store.nodesMap;
      for (let item in nodes) {
        nodes[item].expanded = false;
        if (item === this.defaultKeys[0]) {
          nodes[item].expanded = true;
        }
      }
    },

    // 点击节点选中
    handleNodeClick(node, nodeData) {
      // 人名都是叶子节点且未选中的情况下才传给父组件
      if (nodeData.isLeaf && !nodeData.checked) {
        this.$refs.projectTree.setChecked(nodeData, !node.checked);
        const temp = this.$refs.projectTree.getCheckedNodes();
        const filteredNodes = temp.filter((node) => node.type !== 'depart'); // 过滤部门全选后的depart节点
        this.$emit('change', filteredNodes);
      }
    }
  }
};
</script>

代码到这里就可以结束了,但是我有特别的需求,用户刷新浏览器后,要保留之前选中的数据回显,而且刷新后我能拿到的、传给子组件作为:value的数据是一个字符串数组,形如[‘Lucy’,'Bob','Lucky']这样的人员英文名,所以需要去树结构中,也就是在treeData里匹配出对应的节点数据,然后emit给父组件更:value的值~所以代码又有了部分改动~也一起贴出来

step1: 修改getTreeData函数,如果传给子组件的this.$props.value有值,说明用户进行了刷新操作,且拿到了不为空的字符串数组,需要进行回显。此时,数据回显的匹配得在拿到treeData后才能进行。因为getTreeData是在created生命周期里被调用的,所以只有刷新的时候会执行一次,并不会影响后续父子组件的交互;

getTreeData() {
      CommonService.getAllDepartmentUsers().then((res) => {
        if (res.status == 200) {
          if (res.data.success) {
            this.treeData = res.data.data;
            // 刷新后url中拿到的[‘’]字符串数组,回显到select中
            if (this.$props.value.length > 0) {
              // 根据url的英文名匹配对应树形结构的数据
              this.transformData();
            }
          } else {
            this.$message({ message: res.data.message, type: 'error' });
          }
        } else {
          this.$message({ message: '服务端请求异常', type: 'error' });
        }
      });
    },

step2: 补充transformData()方法,

 // 刷新回显
    findUsers(treeData, usernames) {
      const results = [];
      function search(node) {
        if (node.type === 'user' && usernames.includes(node.userName)) {
          results.push(node);
        }
        if (node.children) {
          node.children.forEach((child) => search(child));
        }
      }
      // 首先确保treeData数组存在并且有内容,然后获取第一个元素
      if (treeData.length > 0 && treeData[0].children) {
        treeData[0].children.forEach((node) => search(node));
      }
      return results;
    },
    transformData() {
      const temp = this.findUsers(this.treeData, this.$props.value);
      // 匹配到的数结构emit给父组件,让父组建更新:value的值-回显
      this.$emit('change', temp);
    }

3.2 父组件使用

 <user-tree-select-mutiple
          :value="filters.createrUsers"
          @change="createrUsersChange"
          :clearable="true"
          style="width: 140px">
 </user-tree-select-mutiple>
    
 data(){
     return {
         filters:{
             createrUsers: [],
         }
     }
 }
 
 
 createrUsersChange(val) {
    this.filters.createrUsers = val;// 赋值-更新子组件:value
    this.queryEventHandle();// 传值给后端-省略
 },
       

注意:此时拿到的val是整个节点的数据,给后端传值的时候,根据需求进行过滤,比如我需要的是人员英文名,选中多个人员,用逗号拼接。那我传给后端的数据应该转换成:

filters.createrUsers = this.filters.createrUsers.map((user) => user.userName).join(',');

四、资料

el-select相关说明
value-key作为 value 唯一标识的键名,绑定值为对象类型时必填
filter-method自定义搜索方法
collapse-tags多选时是否将选中值按文字的形式展示
el-tree相关说明
props配置选项
node-key每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
default-expanded-keys默认展开的节点的 key 的数组
filter-node-method对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏
@check当复选框被点击的时候触发
accordion是否每次只打开一个同级树节点展开
@node-click节点被点击时的回调:共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。
filter对树节点进行筛选操作
setCheckedNodes设置目前勾选的节点,使用此方法必须设置 node-key 属性
setChecked通过 key / data 设置某个节点的勾选状态,使用此方法必须设置 node-key 属性
getCheckedNodes若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组

Plus:这次的需求是基于带我的小姐姐之前的相关代码改出来的,改出来后受益良多~