vue2基于ElementUI组件封装下拉树形选择组件

2,247 阅读3分钟

需求分析

  1. 实现类似 el-select 下拉框与收缩功能
  2. 展开结构为树形组件
  3. 支持数据双向绑定,数据静态搜索

思路

使用el-selectel-tree二次封装完成

基础实现

子组件

<template>
  <el-select
    ref="selectTreeRef"
    v-model="selectedValue">
    <el-option
      v-for="d in option"
      :key="d.id"
      :value="d.id"
      :label="d.label"
      style="display: none"
    >
    </el-option>
    <el-tree
      ref="treeRef"
      :data="treeData"
      node-key="id"
      highlight-current
      :expand-on-click-node="false"
      @current-change="handleCurrentChange"
    >
    </el-tree>
  </el-select>
</template>
<sctipt>
    // 数据
const data = [
  { id: 1, label: '一级 1' },
  { id: 2, label: '一级 2' },
  { id: 3, label: '二级 1-1', pid: 1 },
  { id: 4, label: '二级 1-1', pid: 3 },
]
// 列表转树形结构
function listToTree(list, idField = 'id', parentId = 'pid') {  
  const map = {}; // 用于存储所有节点的映射  
  const tree = []; // 最终的树形结构数组  
 
  // 首先,将列表中的每个项放入映射中,以便稍后快速查找  
  list.forEach(item => {  
    map[item[idField]] = { ...item, children: [] };  
  });  
 
  // 遍历列表,为每个节点找到其父节点,并将其添加到父节点的 children 数组中  
  list.forEach(item => {  
    const parent = map[item[parentId]];
    if (parent) {  
      // 如果找到了父节点,将当前节点添加到父节点的 children 数组中  
      parent.children.push(map[item[idField]]);  
    } else {  
      // 如果没有父节点(即它是根节点),则将其添加到树形结构数组中  
      tree.push(map[item[idField]]);  
   }  
  });  
 
  return tree;  
} 
export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: [String, Number],
  },
  data() {
    return {
      // 下拉选项
      option: data,
      // 树形结构数据
      treeData: listToTree(data),
      // 选中值
      selectedValue: ''
    }
  },
  watch: {
    value: {
      handler(val) {
        this.selectedValue = val;
      },
      immediate: true
    }
  },
  methods: {
    // 当前选中节点变化时触发的事件
    handleCurrentChange(data) {
      this.selectedValue = data.id;

      // 使 input 失去焦点,并隐藏下拉框
      this.$refs.selectTreeRef.blur()

      this.updateValue();
    },
    // 更新value
    updateValue() {
      this.$nextTick(() => {
        this.$emit('change', this.selectedValue);
      })
    }
    
  }
}
</script>

父组件使用

<template>
  <!-- 如果没有全局注册组件,此处需要引入组件 -->
  <tree-select v-model="value"></tree-select>
</template>
<sctipt>
  export default {
    data() {
      return {
        value: ''
      }
    }
  }
</sctipt>

注意:

  1. el-option设置样式style="display: none",如果不设置,上半部分是下拉,下半部分才是el-tree

回显自动展开,并且标亮当前选中项

  1. 自动展开使用 default-expanded-keys
  2. 标亮当前选中项是需要设置 highlight-current,同时监听下拉框展开visible-change设置当前选中项
<template>
  <el-select
    ref="selectTreeRef"
    v-model="selectedValue"
    @visible-change="handleVisibleChange">
    ...
    </el-option>
    <el-tree
     ...
      ref="treeRef"
      highlight-current
      :default-expanded-keys="[selectedValue]"
    >
    </el-tree>
  </el-select>
</template>
<sctipt>
export default {
  methods: {
    // 显示/隐藏下拉框时触发的事件
    handleVisibleChange(val) {
      if(val) {
        this.setCurrentNode()
      }
    },
    // 设置当前选中节点
    setCurrentNode() {
      this.$nextTick(() => {
        // 设置当前选中节点
        this.$refs.treeRef.setCurrentKey(this.selectedValue || null)
      })
    },      
  }
}
</sctipt>

可筛选

  1. 使用 el-select 的自定义搜索
  2. el-tree 设置filter-node-method
  3. 在函数filter-method中调用el-tree实例的filter方法
<template>
  <el-select 
    ref="selectTreeRef"
    v-model="selectedValue"
    filterable
    :filter-method="filterMethod">
    ...
    </el-option>
    <el-tree
     ...
      ref="treeRef"
      :filter-node-method="filterNode"
    >
    </el-tree>
  </el-select>
</template>
<sctipt>
export default {
  methods: {
    // 搜索时触发的事件
    filterMethod(query) {
      this.$refs.treeRef.filter(query);
    },
    // 过滤节点时触发的事件
    filterNode(value, data) {
      if(!value) return true;
      return data.label.indexOf(value) !== -1;
    }  
  }
}
</sctipt>

完整版

<template>
  <el-select
    ref="selectTreeRef"
    v-model="selectedValue"
    filterable
    :filter-method="filterMethod"
    @visible-change="handleVisibleChange">
    <el-option
      v-for="d in option"
      :key="d.id"
      :value="d.id"
      :label="d.label"
      style="display: none"
    >
    </el-option>
    <el-tree
      ref="treeRef"
      :data="treeData"
      node-key="id"
      highlight-current
      :default-expanded-keys="[selectedValue]"
      :expand-on-click-node="false"
      @current-change="handleCurrentChange"
      :filter-node-method="filterNode"
    >
    </el-tree>
  </el-select>
</template>
<sctipt>
    // 数据
const data = [
  { id: 1, label: '一级 1' },
  { id: 2, label: '一级 2' },
  { id: 3, label: '二级 1-1', pid: 1 },
  { id: 4, label: '二级 1-1', pid: 3 },
]
// 列表转树形结构
function listToTree(list, idField = 'id', parentId = 'pid') {  
  const map = {}; // 用于存储所有节点的映射  
  const tree = []; // 最终的树形结构数组  
 
  // 首先,将列表中的每个项放入映射中,以便稍后快速查找  
  list.forEach(item => {  
    map[item[idField]] = { ...item, children: [] };  
  });  
 
  // 遍历列表,为每个节点找到其父节点,并将其添加到父节点的 children 数组中  
  list.forEach(item => {  
    const parent = map[item[parentId]];
    if (parent) {  
      // 如果找到了父节点,将当前节点添加到父节点的 children 数组中  
      parent.children.push(map[item[idField]]);  
    } else {  
      // 如果没有父节点(即它是根节点),则将其添加到树形结构数组中  
      tree.push(map[item[idField]]);  
   }  
  });  
 
  return tree;  
} 
export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: [String, Number],
  },
  data() {
    return {
      // 下拉选项
      option: data,
      // 树形结构数据
      treeData: listToTree(data),
      // 选中值
      selectedValue: ''
    }
  },
  watch: {
    value: {
      handler(val) {
        this.selectedValue = val;
      },
      immediate: true
    }
  },
  methods: {
    // 当前选中节点变化时触发的事件
    handleCurrentChange(data) {
      this.selectedValue = data.id;

      // 使 input 失去焦点,并隐藏下拉框
      this.$refs.selectTreeRef.blur()

      this.updateValue();
    },
    // 更新value
    updateValue() {
      this.$nextTick(() => {
        this.$emit('change', this.selectedValue);
      })
    },
    // 显示/隐藏下拉框时触发的事件
    handleVisibleChange(val) {
      if(val) {
        this.setCurrentNode()
      }else {
        this.$refs.treeRef.filter(null);
      }
    },
    // 设置当前选中节点
    setCurrentNode() {
      this.$nextTick(() => {
        // 设置当前选中节点
        this.$refs.treeRef.setCurrentKey(this.selectedValue || null)
      })
    },
    // 搜索时触发的事件
    filterMethod(query) {
      
      this.$refs.treeRef.filter(query);
    },
    // 过滤节点时触发的事件
    filterNode(value, data) {
      if(!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    // 获取上级数据
    queryAllSuperiorData() {
      let {
        treeData,
        selectedValue
      } = this;
      let valueKey = 'id';
      function findAncestors(nodes, targetId, ancestors = []) {
        for (let node of nodes) {
          if (node[valueKey] === targetId) {
            return ancestors;
          }
          if (node.children && node.children.length > 0) {
            ancestors.push(node);
            const result = findAncestors(node.children, targetId, ancestors.slice());
            if (result) return result;
            ancestors.pop();
          }
        }
          return null;
       }
      return findAncestors(treeData, selectedValue);
    }
    
  }
}
</script>

案例中使用的下拉数据现在是固定的,有需要可以自己改成props传参,筛选也是默认开启,也可以改成props控制。

本案例使用树形单选下拉,如果需要多选就需要各位大神自己修改。

Vue2+elementUi 封装树形下拉(单选,多选,筛选) - 码上掘金 (juejin.cn)