Element UI 二次封装实现下拉树组件

使用element ui 二次封装实现下拉树组件,可以搜索,可以动态绑定

一 实现效果图

1.1 单选模式

单选模式.gif

1.2 多选模式

多选模式.gif

二 相关说明

2.1 框架以及版本

    1、element-ui 2.15.1 2
    2、vue 2.6.11

2.2 组件概述

封装的下拉树,结合了element ui中的下拉组件 el-select 与 树结构组件 el-tree,取两个组件中的一些属性,在此基础上进行的封装。其中可通过v-model对数据进行动态绑定。自行封装扩展性不高,可能会有一些问题,需求不同,可自行进行封装。

2.3 组件api

  1. 相关属性
属性 说明 类型 默认值
defaultValue/v-model String, Number, Array -
treeData 树结构数据源 Array []
treeProps 替换treeNode 中 label, children 字段为 treeData 中对应的字段 Object {label: ‘name’, children: ‘children’}
nodeKey 树结构数据key取值,数据唯一,默认id String id
clearable 是否显示清空功能 Boolean false
multiple 是否多选 Boolean false
placeholder 选择框默认文字 String 请选择
showSearch 是否显示搜索框 Boolean false
searchPlaceholder 搜索框默认文字 String 请输入关键字
collapseTags 多选时是否将选中值按文字的形式展示 Boolean true

2 相关方法

名称 说明 参数
change 数据发生变化 数据,选中的节点
removeTag 多选模式下,点击删除单个tag触发 当前选中的值
clear 清空选项触发 -

三 代码实现

3.1 组件页面代码

<template>
  <el-select
    ref="treeSelectRef"
    style="width: 100%;"
    clearable
    :value="selectLabel"
    :multiple="multiple"
    :placeholder="placeholder"
    :popper-class="popperClass"
    :collapse-tags="collapseTags"
    @remove-tag="handleSelectRemoveTag"
    @clear="handleSelectClear"
  >
    <div class="tree-select-search" v-if="showSearch">
      <el-input
        placeholder="请输入关键字"
        v-model="treeFilterText"
        clearable
        size="mini"
        >
      </el-input>
    </div>
    <el-option :value="selectOptionValue"
      class="tree-select-option-item"
    >
      <div class="tree-select-tree-body">
        <el-tree
          id="tree-option"
          ref="optionTreeRef"
          highlight-current
          empty-text="加载中,请稍后......"
          :show-checkbox="showCheckbox"
          :data="treeData" 
          :props="treeProps"
          :node-key="nodeKey"
          :filter-node-method="filterTreeNode"
          :default-expanded-keys="defaultExpandedKeys"
          @node-click="handleNodeClick"
          @check="handleTreeCheck">
        </el-tree>
      </div>
    </el-option>
  </el-select>
</template>

3.2 组件JS部分代码

<script>
const defaultProps = {
  children: 'children', // 子级字段
  label: 'name' // 展示字段
}

export default {
  name: 'CustomTreeSelect',
  model: {
    prop: "defaultValue",
    event: "change"
  },
  props: {
    // v-model值
    defaultValue: {
      type: [String, Number, Array]
    },
    // 数据源
    treeData: {
      type: Array,
      default () {
        return []
      }
    },
    // 替换 treeNode 中 label, children 字段为 treeData 中对应的字段
    treeProps: {
      type: Object,
      default () {
        return defaultProps
      }
    },
    // 树结构key的取值,数据唯一, 默认为id
    nodeKey: {
      type: String, default: 'id'
    },
    // 是否显示清空功能
    clearable: {
      type: Boolean, default: false
    },
    // 是否多选 
    multiple: {
      type: Boolean, default: false
    },
    // 选择框默认文字
    placeholder: {
      type: String, default: '请选择'
    },
    // 搜索框默认文字
    searchPlaceholder: {
      type: String, default: '请输入关键字'
    },
    // 是否显示搜索框
    showSearch: {
      type: Boolean, default: false
    },
    // 多选时是否将选中值按文字的形式展示
    collapseTags: {
      type: Boolean, default: true
    }
  },
  data() {
    return {
      // 下拉框选中值
      selectValue: '',
      // 下拉框选中回显值
      selectLabel: '',
      // 下拉框Option的值,设置为undefined,防止选中
      selectOptionValue: undefined,
      // 树数据搜索内容
      treeFilterText: '',
      // 默认展开
      defaultExpandedKeys: []
    }
  },
  computed: {
    // 树结构是否显示复选框(多选情况下出现)
    showCheckbox () {
      return this.multiple
    },
    // select 下拉框自定义的类名-可自行修改
    popperClass () {
      let classNames = ['custom-tree-select-popper']
      if (this.showSearch) {
        classNames.push('custom-tree-select-search')
      }
      return classNames.join(' ')
    },
    // 下拉框实例
    treeSelectRef () {
      return this.$refs.treeSelectRef
    },
    // 树结构实例
    optionTreeRef () {
      return this.$refs.optionTreeRef
    }
  },
  watch: {
    // 监听默认值,对数据赋值
    defaultValue: {
      deep: true,
      immediate: true,
      handler (newValue, oldValue) {
        const { multiple } = this
        if (newValue) {
          if (!this.reloadTreeCheck)
            this.handleSetTreeCheck(newValue);
        } else {
          this.selectValue = multiple ? [] : ''
          this.selectLabel = multiple ? [] : ''
        }
      }
    },
    treeFilterText (value) {
      setTimeout(() => {
        this.handleInputChange(value)
      }, 300)
    }
  },
  mounted () {
    // 重新加载树结构选中 false-加载 true-不加载
    this.reloadTreeCheck = false
  },
  methods: {
    /** 多选模式下,点击移除单个tag */
    handleSelectRemoveTag (tag) {
      this.reloadTreeCheck = false
      const { selectValue } = this
      if (isArray(selectValue)) {
        this.selectValue.shift()
      }
      this.$emit('removeTag', tag)
    },
    /** select框的清除按钮 */
    handleSelectClear () {
      this.reloadTreeCheck = false
      const { multiple } = this
      const value = multiple ? [] : ''
      this.treeFilterText = ''
      this.selectValue = [ ...value ]
      this.selectLabel = [ ...value ]
      this.$emit('change', value, null)
      this.$emit('clear')
    },
    /** 树节点 点击时 */
    handleNodeClick (node) {
      const { treeProps, multiple, nodeKey } = this
      if (multiple) return 
      if (node.children && node.children.length > 0) return
      this.selectValue = node[nodeKey]
      this.selectLabel = node[treeProps.label]
      this.$emit('change', this.selectValue, node)
      // 下拉框失去焦点,隐藏下拉面板
      this.treeSelectRef.blur()
      // this.treeFilterText = ''
    },
    /** 树节点 复选框选中时 */
    handleTreeCheck (node, values) {
      const { treeProps } = this
      const { checkedKeys, checkedNodes } = values
      this.selectValue = checkedKeys
      const lableValues = checkedNodes.map(nodeItem => {
        return nodeItem[treeProps.label]
      })
      this.selectLabel = lableValues
      this.$emit('change', checkedKeys, checkedNodes)
    },
    /** 设置回显数据 */
    handleSetTreeNode (value) {
      const { optionTreeRef, treeProps } = this
      this.selectValue = value
      if (isArray(value)) { 
        // 多选
        this.selectLabel = value.map(item => {
          const treeNode = optionTreeRef.getNode(item)
          return treeNode.data[treeProps.label]
        })
      } else {
        // 单选
        const treeNode = optionTreeRef.getNode(value)
        this.selectLabel = treeNode.data[treeProps.label]
      }
    },
    /** 处理数据树结构展开,并处理选中效果 */
    handleTreeExpandKeys (value) {
      const { optionTreeRef, multiple } = this
      if (isArray(value) && multiple) {
        optionTreeRef.setCheckedKeys(value)
        value.forEach(item => {
          const treeNode = optionTreeRef.getNode(item)
          if (treeNode && treeNode.parent) {
            this.setTreeExpandKeys(treeNode.parent)
          }
        })
      } else {
        optionTreeRef.setCurrentKey(value)
        const treeNode = optionTreeRef.getNode(value)
        if (treeNode && treeNode.parent) {
          this.setTreeExpandKeys(treeNode.parent)
        }
      }
    },
    /** 处理树结构父级展开 */
    setTreeExpandKeys (node) {
      node.expanded = true
      if (node.parent) {
        this.setTreeExpandKeys(node.parent)
      }
    },
    /** 搜索树节点 */
    filterTreeNode (value, data, node) {
      if (!value) {
        // if (node.expanded) node.expanded = false
        return true
      }
      const { treeProps: { label } } = this
      return data[label].indexOf(value) !== -1;
    },
    /** 设置树结构回显选中 */
    handleSetTreeCheck (value) {
      if (!this.reloadTreeCheck && value) {
        this.$nextTick(() => {
          this.handleSetTreeNode(value)
          this.handleTreeExpandKeys(value)
          this.reloadTreeCheck = true
        })
      }
    },
    /** 搜索框中按下回车失去焦点触发 */
    handleInputChange (value) {
      this.optionTreeRef.filter(value)
    }
  }
}

/**
 * 判断数据类型是否为数组
 */
function isArray(arg) {
  if (typeof Array.isArray === 'undefined') {
    return Object.prototype.toString.call(arg) === '[object Array]'
  }
  return Array.isArray(arg)
}
</script>

3.3 组件样式代码

这里有覆盖修改到element-ui的原始样式,
注:如果需要修改下拉框的高度,修改样式代码中.el-select-dropdown__wrap 中的高度属性

<style lang="scss">
.custom-tree-select-search {
  .el-select-dropdown__list {
    padding: 0;
  }
}
.custom-tree-select-popper {
  .el-scrollbar {
    .el-select-dropdown__wrap {
      max-height: 365px !important;
    }
    .el-scrollbar__bar.is-vertical {
      z-index: 3;
    }
  }
}
</style>
<style lang="scss" scoped>
.tree-select-search {
  position: sticky;
  top: 0;
  z-index: 2;
  display: block;
  padding: 6px;
  background: #fff;
}
.tree-select-option-item {
  background: #fff;
  overflow: scroll;
  height: 200px;
  overflow-x: hidden;
}
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item{
  height: auto;
  min-height: 200px;
  padding: 0;
  overflow: hidden;
}
</style>

四 如何使用

4.1 组件引入注册后页面

<CustomTreeSelect
  clearable
  multiple
  show-search
  nodeKey="code"
  v-model="treeValue"
  :treeData="treeData"
  :treeProps="treeProps"
/>

4.2 参数设置

export default {
  data () {
     return {
        treeData: [
        {
          id: "321C8FF6CC6046D79CD8877526054BCF",
          text: "1100 北京市本级",
          name: "1100 北京市本级",
          code: "1100"
        },
        {
          id: "157EAB9EA1A34D0F824E97C2C7D9F0CA",
          text: "1101 市辖区",
          name: "1101 市辖区",
          code: "1101",
          children: [
            {
              id: "B08F7DB39F124ACFB2171508D8C5C0FD",
              text: "110101 东城区",
              name: "110101 东城区",
              code: "110101"
            },
            {
              id: "2AF3A9766AAD433A95D4EADE5FB97839",
              text: "110102 西城区",
              name: "110102 西城区",
              code: "110102"
            },
            {
              id: "A2058EAE12674B3D9BA3710C87DDB111",
              text: "110105 朝阳区",
              name: "110105 朝阳区",
              code: "110105"
            },
            {
              id: "C6BC2FE9FE86493291EE59171133ABB5",
              text: "110106 丰台区",
              name: "110106 丰台区",
              code: "110106"
            },
            {
              id: "06E478CB634F4FC19EC0381D55751218",
              text: "110107 石景山区",
              name: "110107 石景山区",
              code: "110107"
            },
            {
              id: "DD0B1A29473D4053989938700B73AE26",
              text: "110108 海淀区",
              name: "110108 海淀区",
              code: "110108"
            },
            {
              id: "5D3FF6E7729E43F9B813EAAB80796E41",
              text: "110109 门头沟区",
              name: "110109 门头沟区",
              code: "110109"
            },
            {
              id: "349609893FAC47BC97509B8FC411059A",
              text: "110111 房山区",
              name: "110111 房山区",
              code: "110111"
            },
            {
              id: "1E5B2DC4A1E84C959BB78D6E1B49A1DF",
              text: "110112 通州区",
              name: "110112 通州区",
              code: "110112"
            },
            {
              id: "2C9DC40AEFF8454ABF8A2EE6AD0A2DB5",
              text: "110113 顺义区",
              name: "110113 顺义区",
              code: "110113"
            },
            {
              id: "5C0342E4E41841AD837E365DB79E81B7",
              text: "110114 昌平区",
              name: "110114 昌平区",
              code: "110114"
            },
            {
              id: "DF25016580634FD39307FC0E18E7CF4F",
              text: "110115 大兴区",
              name: "110115 大兴区",
              code: "110115"
            },
            {
              id: "8DC99ABF109D40ED899552A08FE07C63",
              text: "110116 怀柔区",
              name: "110116 怀柔区",
              code: "110116"
            },
            {
              id: "9D2DA21BD26C40CC90B8BB26E1B437AC",
              text: "110117 平谷区",
              name: "110117 平谷区",
              code: "110117"
            },
            {
              id: "AD5172DDD7D547F5A06F022D64EB4DF5",
              text: "110118 密云区",
              name: "110118 密云区",
              code: "110118"
            },
            {
              id: "78B4DA4936A44314AD801B15988707D4",
              text: "110119 延庆区",
              name: "110119 延庆区",
              code: "110119"
            }
          ]
        },
        {
          id: "E8ADAB8AFDB24F77B3FA89ED7E9A9F15",
          text: "1102 县",
          name: "1102 县",
          code: "1102"
        }
      ],
      treeProps: {
        label: 'name',
        children: 'children'
      },
      treeValue: [],
    }
  }
}

参考文章

https://element.eleme.cn/#/zh-CN
https://blog.csdn.net/R_xxxxx/article/details/106112583
https://blog.csdn.net/sleepwalker_1992/article/details/87894588
https://www.jianshu.com/p/47e13795cfe7
https://blog.csdn.net/Mrchai521/article/details/114993368
https://blog.csdn.net/qq_41791303/article/details/103710456

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341