el-select 和el-tree二次封装

前言

本文章是本人在开发过程中,遇到使用树形数据,动态单选或多选的需求,element中没有这种组件,故自己封装一个,欢迎多多指教

开发环境:element-UI、vue2

组件效果

单选

多选

组件引用

html 复制代码
<treeselect v-model="form.parentId"
                      :options="deptOptions"
                      :props="{value:'id',label:'name',children: 'children'}"
                      :placeholder="'选择上级部门'"
          />

组件代码

html 复制代码
<template>
  <div>
    <el-select
      ref="treeSelect"
      popper-class="custom-select-popper"
      style="width: 100%"
      v-model="valueLabel"
      :clearable="clearable"
      :placeholder="placeholder"
      :multiple="multiple"
      @clear="handleClear"
      @remove-tag="handleRemoveTag"
    >
      <el-input v-if="filter"
                v-model="filterText"
                :placeholder="filterPlaceholder" style="margin-top: -6px;"
      />
      <el-option :value="value" :label="option.name" class="select-options">
        <el-tree
          id="tree-option"
          ref="treeSelectTree"
          :accordion="accordion"
          :data="options"
          :props="props"
          :node-key="props.value"
          :highlight-current="!multiple"
          :show-checkbox="multiple"
          :check-strictly="checkStrictly"
          :default-expand-all="expandAll"
          :expand-on-click-node="multiple"
          :filter-node-method="filterNode"
          @node-click="handleNodeClick"
          @check="handleNodeCheckbox"
        >
          <span slot-scope="{ node, data }" class="tree_label">
                {{ node.label }}
              </span>
        </el-tree>
      </el-option>
    </el-select>
  </div>
</template>
<script>
export default {
  name: 'TreeSelect',
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: {
      type: [String, Number, Object, Array],
      default: () => {
        return ''
      }
    },
    clearable: {
      type: Boolean,
      default: true
    },
    placeholder: {
      type: String,
      default: '请选择'
    },
    multipleLimit: {
      type: Number,
      default: 2
    },
    //--------- filter props -----
    filter: {
      type: Boolean,
      default: true
    },
    filterPlaceholder: {
      type: String,
      default: '请输入关键字'
    },
    //----- tree props -----
    accordion: {
      type: Boolean,
      default: false
    },
    options: {
      type: Array,
      default: () => {
        return []
      }
    },
    props: {
      type: Object,
      default: () => {
        return {
          value: 'id',
          label: 'label',
          children: 'children'
        }
      }
    },
    expandAll: {
      type: Boolean,
      default: false
    },
    checkStrictly: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      tp: {
        value: 'id',
        label: 'label',
        children: 'children',
        prentId: 'parentId'
      },
      multiple: false,
      valueLabel: [],
      option: {
        id: '',
        name: ''
      },
      filterText: undefined,
      valueId: [],
      treeIds: []
    }
  },
  watch: {
    valueId() {
      if (this.multiple) {
        let valueStr = ''
        if (this.value instanceof Array) {
          valueStr = this.value.join()
        } else {
          valueStr = '' + this.value
        }
        if (valueStr !== this.valueId.join()) {
          this.$emit('change', this.valueId)
        }
      } else {
        let id = this.valueId.length > 0 ? this.valueId[0] : undefined
        if (id !== this.value) {
          this.$emit('change', id)
        }
      }
    },
    value: {
      handler(newVal, oldVal) {
        if (newVal !== oldVal) {
          this.init()
        }
      }
    },
    filterText: {
      handler(newVal, oldVal) {
        if (newVal !== oldVal) {
          this.$refs.treeSelectTree.filter(newVal)
        }
      }
    }
  },
  mounted() {
    for (let key in this.tp) {
      if (this.props[key] !== undefined) {
        this.tp[key] = this.props[key]
      }
    }
    this.multiple = this.multipleLimit > 1
    this.init()
    this.$nextTick(() => {
      if (this.multiple) {
        document.getElementsByClassName('el-select__tags')[0].style.maxHeight = document.getElementsByClassName('el-select')[0].offsetHeight * 2 - 4 + 'px'
      }
    })

  },
  methods: {
    init() {
      if (this.value instanceof Array) {
        this.valueId = this.value
      } else if (this.value === undefined) {
        this.valueId = []
      } else {
        this.valueId = [this.value]
      }
      if (this.multiple) {
        for (let id of this.valueId) {
          this.$refs.treeSelectTree.setChecked(id, true, false)
        }
      } else {
        this.$refs.treeSelectTree.setCurrentKey(this.valueId.length > 0 ? this.valueId[0] : undefined)
      }
      this.initValueLabel()
      this.initTreeIds()
      this.initScroll()
    },
    // 初始化滚动条
    initScroll() {
      this.$nextTick(() => {
        let scrollWrap = document.querySelectorAll('.el-scrollbar .el-select-dropdown__wrap')[0]
        scrollWrap.style.cssText = 'margin: 0px; max-height: none; overflow: hidden;'
        let scrollBar = document.querySelectorAll('.el-scrollbar .el-scrollbar__bar')
        scrollBar.forEach((ele) => (ele.style.width = 0))
      })
    },
    initTreeIds() {
      let treeIds = []
      let _this = this

      function traverse(nodes) {
        for (let node of nodes) {
          treeIds.push(node[_this.tp.value])
          if (node[_this.tp.children]) {
            traverse(node[_this.tp.children])
          }
        }
      }

      traverse(this.options)
      this.treeIds = treeIds
    },
    initValueLabel() {
      let labels = []
      let _this = this
      for (let id of this.valueId) {
        let node = this.traverse(this.options, node => node[_this.tp.value] === id)
        if (node) {
          labels.push(node[_this.tp.label])
        }
      }
      if (this.multiple) {
        this.valueLabel = labels
        this.option.name = labels.join()
      } else {
        this.valueLabel = labels.length > 0 ? labels[0] : undefined
        this.option.name = this.valueLabel
      }
    },
    traverse(tree, func) {
      for (let node of tree) {
        if (func(node)) {
          return node
        }
        if (node[this.tp.children]) {
          let result = this.traverse(node[this.tp.children], func)
          if (result !== undefined) {
            return result
          }
        }
      }
      return undefined
    },
    handleClear() {
      this.valueLabel = []
      this.valueId = []
      if (this.multiple) {
        for (let id of this.treeIds) {
          this.$refs.treeSelectTree.setChecked(id, false, false)
        }
      } else {
        this.$refs.treeSelectTree.setCurrentKey(null)
      }
    },
    /* 树filter方法 */
    filterNode(value, data) {
      if (!value) return true
      return data[this.props.label].indexOf(value) !== -1
    },
    /* 树节点点击事件 */
    handleNodeClick(data, node) {
      if (!this.multiple) {
        this.filterText = ''
        this.valueId = [data[this.tp.value]]
      }
      if(node.childNodes){
        node.expanded = true
      }
    },
    handleNodeCheckbox(data, node) {
      if (this.multiple) {
        if (this.multipleLimit >= node.checkedKeys.length) {
          this.valueId = node.checkedKeys
        } else {
          this.$refs.treeSelectTree.setChecked(data, false, !this.checkStrictly)
          this.$message.error('最多选择' + this.multipleLimit + '项')
        }
      }
    },
    handleRemoveTag(tag) {
      let n = this.traverse(this.options, node => node[this.tp.label] === tag)
      if (n) {
        this.$refs.treeSelectTree.setChecked(n[this.tp.value], false, !this.checkStrictly)
      }
      this.valueId = this.$refs.treeSelectTree.getCheckedKeys()
    }
  }
}

</script>

<style scoped lang="scss">
::v-deep .el-select__tags {
  overflow: auto;
}
.custom-select-popper{

}

.el-scrollbar {
  .el-scrollbar__view {
    .el-select-dropdown__item {
      height: auto;
      max-height: 300px;
      padding: 0;
      overflow: hidden;
      overflow-y: auto;
    }

    .el-select-dropdown__item.selected {
      font-weight: normal;
    }
  }
}

ul li {
  .el-tree {
    .el-tree-node__content {
      height: auto;
      padding: 0 20px;
    }
    .el-tree-node__label {
      font-weight: normal;
    }
    .is-current > .el-tree-node__label{
      color: #409eff;
      font-weight: 700;
    }
  }
}


.tree_label {
  line-height: 23px;

  .label_index {
    background-color: rgb(0, 175, 255);
    width: 22px;
    height: 22px;
    display: inline-flex;
    border-radius: 4px;

    .label_index_font {
      color: #ffffff;
      width: 100%;
      text-align: center;
    }
  }
}
</style>
相关推荐
前端大卫9 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘25 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare26 分钟前
浅浅看一下设计模式
前端
Lee川29 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端