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>
相关推荐
郑州光合科技余经理4 小时前
本地生活服务系统:成品模块和定制接口怎么划界
java·前端·人工智能·后端·系统架构·php·ai编程
平头哥~7 小时前
Day 07 _ 那条 1px 的线,为什么在手机上忽粗忽细
前端·css·样式·学习资料
小江的记录本8 小时前
【CSS】CSS 动画:transition、animation、transform、CSS3 新特性(附《思维导图》)
前端·css·安全·spring·前端框架·css3·动画
circuitsosk8 小时前
任务规划器的三种范式对比:ReAct、Plan-and-Execute 与 Tree-of-Thought 在真实业务中的取舍
前端·javascript·python·react.js·llm·ai agent
hzxpaipai9 小时前
企业官网技术架构拆解:前端、后台、数据库、服务器如何协同
前端·数据库·架构
深念Y11 小时前
Lenovo XiaoXinAir 14 — 性能模式切换
前端·网络
计算机魔术师12 小时前
Meta突然杀进第一梯队:一个新模型,把AI推理成本打下来8倍
前端
IT_陈寒12 小时前
Redis缓存雪崩把我坑惨了,这次长记性了
前端·人工智能·后端
风骏时光牛马12 小时前
提示词工程:大模型效能挖掘与指令设计实战
前端