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>
相关推荐
蟾宫曲2 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心2 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
qq13267029402 小时前
运行Zr.Admin项目(前端)
前端·vue2·zradmin前端·zradmin vue·运行zradmin·vue2版本zradmin
LCG元3 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
魏时烟3 小时前
css文字折行以及双端对齐实现方式
前端·css
哥谭居民00014 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~4 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
2401_882726484 小时前
低代码配置式组态软件-BY组态
前端·物联网·低代码·前端框架·编辑器·web
web130933203984 小时前
ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
前端·github