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

414 阅读3分钟

需求分析

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

思路

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

基础实现

子组件

js
 代码解读
复制代码
<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>

父组件使用

js
 代码解读
复制代码
<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设置当前选中项
js
 代码解读
复制代码
<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方法
js
 代码解读
复制代码
<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>

完整版

js
 代码解读
复制代码
<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;
    }
    
  }
}
</script>