Bootstrap

Vue组件(二) - 二次封装Element UI实现TreeSelect 树形选择器

功能描述

单选:

  • 默认初始值;
  • select输入框清空功能;
  • value数据双向绑定;
    在这里插入图片描述

多选:

  • 默认初始值;
  • select输入框清空功能;
  • select输入框中的×删除勾选;
  • 支持角色菜单控制功能:check-strictly=true
    • 当点击勾选复选框时候,若状态为 选中
      • 其所有父节点 (父节点、父节点的父节点以此类推)全部统一跟随当前节点变化为选中;
      • 其所有子节点不跟随当前节点变化
    • 当点击勾选复选框时候,若状态为 未选中
      • 其所有父节点 不跟随当前节点变化;
      • 其所有子节点 全部统一跟随当前节点变化为 未选中;

check-strictly=false
从check-strictly=false父子互相关联的基础入手,需要解决的问题就是:
将尚未全部勾选的子节点对应的父节点改为半勾选状态,查找文档良久无果;
只有getHalfCheckedKeys和getHalfCheckedNodes,并没有设置成半勾选。

在这里插入图片描述
在这里插入图片描述

代码

treeSelect.vue

<!--
 * @Description: 树形选择器
 * @Author: HMM
 * @Date: 2021-05-24 10:27:34
 * @FilePath: \components\tree-select.vue
-->
<template>
  <div class="treeSelect">
    <el-select
      :value="valueTitle"
      :multiple="multiple"
      :collapse-tags="collapse"
      :clearable="clearable"
      :disabled="disabled"
      :size="size"
      :style="selectStyle"
      @input="$emit('input',$event)"
      @clear="handleClear"
      @remove-tag="handleRemoveTag"
    >
      <el-option :value="valueId" :label="label">
        <el-tree
          id="tree-option"
          ref="selectTree"
          :accordion="accordion"
          :show-checkbox="multiple"
          :data="options"
          :props="treeProps"
          :node-key="treeProps.id"
          :check-strictly="checkStrictly"
          :default-expand-all="expand"
          :auto-expand-parent="expandParent"
          :expand-on-click-node="expandNode"
          :default-expanded-keys="defaultExpandedKeys"
          @node-click="handleNodeClick"
          @check-change="handleCheckChange"
        >
        </el-tree>
      </el-option>
    </el-select>
  </div>
</template>



<style lang="scss" scoped>
 .el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
  height: auto;
  // max-height: 274px;
  padding: 0;
  overflow: hidden;
  // overflow: hidden;
  // overflow-y: auto;
}

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

// 横向滚动条
.el-scrollbar__bar.is-horizontal {
  height: 6px;
  left: 2px;
}
// 纵向滚动条
.el-scrollbar__bar.is-vertical {
  width: 6px;
  top: 2px;
}


// 字体和大小
.custom-tree-node {
  font-family:"Microsoft YaHei";
  font-size: 14px;
  position: relative;
}

// 原生el-tree-node的div是块级元素,需要改为inline-block,才能显示滚动条
.treeSelect .el-tree >.el-tree-node {
  display: inline-block;
  min-width: 100%;
}

// ul li ::v-deep .el-tree .el-tree-node__content {
//   height: auto;
//   padding: 0 20px;
// }

// .el-tree-node__label {
//   font-weight: normal;
// }


.el-tree ::v-deep .is-current .el-tree-node__label {
  color: #1B65B9;
  font-weight: 700;
}

.el-tree ::v-deep .is-current .el-tree-node__children .el-tree-node__label {
  color: #606266;
  font-weight: normal;
}
</style>


<script>
export default {
  name: 'tree-select',
  props:{
    // 是否可多选,默认单选
    multiple: {
      type: Boolean,
      default: false
    },
    // 可清空选项
    clearable:{
      type:Boolean,
      default:() => { return true }
    },
    // -------------------- el-tree --------------------
    // 配置项
    treeProps:{
      type: Object,
      default:() => {
        return {
          id:'id', // ID字段名
          label: 'title', // 显示名称
          children: 'children' // 子级字段名
        }
      }
    },
    // 选项列表数据(树形结构的对象数组)
    options:{
      type: Array,
      default: () => { return [] }
    },
    // 自动收起
    accordion:{
      type:Boolean,
      default:() => { return false }
    },
    // 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false
    checkStrictly:{
      type:Boolean,
      default:() => { return false }
    },
    // 是否展开所有节点,默认展开
    expand: {
      type: Boolean,
      default() {
        return true;
      }
    },
    // 展开子节点的时候是否自动展开父节点 默认值为 true
    expandParent:{
      type: Boolean,
      default() {
        return true;
      }
    },
    // 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
    expandNode: {
      type: Boolean,
      default() {
        return true;
      }
    },
    // -------------------- el-select --------------------
    // 初始值 - 绑定value是为了外面也可以传值改变到里面的值双向绑定
    value: {
      type: [String, Number, Boolean, Array],
      default: () => { return null }
    },
    // 多选时是否将选中值按文字的形式展示
    collapse: {
      type: Boolean,
      default: false
    },
    // 选择框大小
    size:{
      type:String,
      default:() => { return 'small' }
    },
    // 选择框 宽度
    width: {
      type: String,
      default: '270px'
    },
    // 是否禁用
    disabled:{
      type:Boolean,
      default:() => { return false }
    }
  },
  data() {
    return {
      valueId: this.value, // 初始值
      valueTitle: '',
      label:'', // 分组
      defaultExpandedKeys:[]
    }
  },
  mounted(){
    this.initHandle();
  },
  methods: {
    /**
     * @description: 初始化组件
     * @param {*}
     * @return {*}
     */
    initHandle(){
      // 单选
      if(this.valueId && !this.multiple){
        var node = this.$refs.selectTree.getNode(this.valueId);
        if(node){
          this.valueTitle = node.data[this.treeProps.label]; // 初始化显示
          this.defaultExpandedKeys = [this.valueId]; // 设置默认展开
          this.$nextTick(() => {
            this.$refs.selectTree.setCurrentKey(this.valueId); // 设置默认选中
          })
        }
      }
      // 多选
      if(this.valueId && this.multiple){
        this.defaultExpandedKeys = this.valueId; // 设置默认展开
        this.$nextTick(() => {
          this.$refs.selectTree.setCheckedKeys(this.valueId); // 设置默认选中
        })
      }
    },
    /**
     * @description: 单选 - 节点被点击时的回调,返回被点击的节点数据
     * @param {*}
     * @return {*}
     */
    handleNodeClick(node){
      this.valueTitle = node[this.treeProps.label];
      this.valueId = node[this.treeProps.id];
      this.defaultExpandedKey = [];
      this.$emit('getValue', this.valueId, this.valueTitle);
    },
    /**
     * @description: 多选,节点勾选状态发生变化时的回调
     * @param {*}
     * @return {*}
     */
    handleCheckChange (data, checked) {
      let currentNode = this.$refs.selectTree.getNode(data);
      if(this.checkStrictly){
        // 用于:父子节点严格互不关联时,父节点勾选变化时通知子节点同步变化,实现单向关联
        if(checked) {
          // 选中 子节点只要被选中父节点就被选中
          this.parentNodeChange(currentNode);
        } else {
          // 未选中 处理子节点全部未选中
          this.childNodeChange(currentNode);
        }
      }
      // 用于:父子节点严格关联时
      // console.log(this.$refs.selectTree.getHalfCheckedKeys())
      this.valueId = this.$refs.selectTree.getCheckedKeys();
      var checkedNodes = this.$refs.selectTree.getCheckedNodes();
      this.valueTitle = checkedNodes.map((node) => {
        return node[this.treeProps.label];
      });
      this.defaultExpandedKeys = [];
      this.$emit('getValue', this.valueId, this.valueTitle);
    },
    /**
     * @description: 清除选中
     * @param {*}
     * @return {*}
     */
    handleClear(){
      this.valueTitle = '';
      this.valueId = '';
      this.defaultExpandedKeys = [];
      // 清除树
      if(this.multiple){
        this.$refs.selectTree.setCheckedKeys(null);
      } else {
        this.$refs.selectTree.setCurrentKey(null);
      }
      this.$emit('getValue', null);
    },
    /**
     * @description: 多选 删除任一标签选项的回调
     * @param {*}
     * @return {*}
     */
    handleRemoveTag(val){
      var checkedNodes = this.$refs.selectTree.getCheckedNodes();
      var node = checkedNodes.find(node => node[this.treeProps.label] === val);
      this.$refs.selectTree.setChecked(node[this.treeProps.id], false);
    },
    // 统一处理子节点为不选中
    childNodeChange (node) {
      for(let i = 0; i < node.childNodes.length; i++) {
        node.childNodes[i].checked = false;
        this.childNodeChange(node.childNodes[i]);
      }
    },
    // 统一处理父节点为选中
    parentNodeChange (node) {
      if(node.parent.key !== undefined) {
        node.parent.checked = true;
        this.parentNodeChange(node.parent);
      }
    }
  },
  watch: {
    value(){
      this.valueId = this.value
      this.initHandle()
    },
    // 父子组件双向绑定value
    valueId(){
      this.$emit('input', this.valueId);
    }
  },
  computed:{
    selectStyle() {
      return {
        width: `${this.width}`
      };
    }
  }
};
</script>


treeSelectDemo.vue


<!--
 * @Description: 树形下拉框 - 使用示例
 * @Author: HMM
 * @Date: 2021-01-14 16:04:43
 * @FilePath: \treeSelectDemo.vue
-->

<template>
  <div class="messageDemo">
    <el-container>
      <el-main>
        <treeSelect
          :treeProps="props"
          :options="treeSelectList"
          v-model="valueId"
          :clearable="isClearable"
          :accordion="isAccordion"
          :expandNode="false"
          size="small"
          width="100%"
          @getValue="getValue($event)"
        />
      </el-main>
      <el-main>
        <span>父子不互相关联:check-strictly=true</span>
        <treeSelect
          :multiple="true"
          :collapse="false"
          :checkStrictly="true"
          :treeProps="props"
          :options="treeSelectList"
          v-model="valueIds"
          :clearable="isClearable"
          :accordion="isAccordion"
          :expandNode="false"
          size="small"
          width="360px"
          @getValue="getValue2($event)"
        />
      </el-main>
      <el-main>
        <span>父子互相关联:check-strictly=false</span>
        <treeSelect
          :multiple="true"
          :collapse="false"
          :treeProps="props"
          :options="treeSelectList"
          v-model="valueIds2"
          :clearable="isClearable"
          :accordion="isAccordion"
          :expandNode="false"
          size="small"
          width="360px"
          @getValue="getValue2($event)"
        />
      </el-main>
    </el-container>
  </div>
</template>


<!-- JS -->
<script>
import treeSelect from '../components/tree-select';
export default {
  name:'demo',
  components: {
    treeSelect
  },
  data() {
    return {
      props:{ // 配置项(必选)
        id: 'id',
        label: 'name',
        pid: 'parentId',
        children: 'children'
        // disabled:true
      },
      // 数组
      list: [
          {id:1, parentId:0, name:'一级菜单A', rank:1},
          {id:2, parentId:0, name:'一级菜单B', rank:1},
          {id:3, parentId:0, name:'一级菜单C', rank:1},
          {id:4, parentId:1, name:'二级菜单A-A', rank:2},
          {id:5, parentId:1, name:'二级菜单A-B', rank:2},
          {id:6, parentId:2, name:'二级菜单B-A', rank:2},
          {id:7, parentId:4, name:'三级菜单A-A-A', rank:3},
          {id:15, parentId:0, name:'一级菜单C', rank:1},
          {id:16, parentId:0, name:'一级菜单C', rank:1},
          {id:17, parentId:0, name:'一级菜单C', rank:1},
          {id:18, parentId:0, name:'一级菜单C', rank:1}
      ],
      treeSelectList:[],
      isClearable:true, // 可清空(可选)
      isAccordion:false, // 可收起(可选)
      valueId:null, // 初始ID(可选)
      valueIds:[],
      valueIds2:[]
    }
  },
  created(){
    this.initData();
  },
  methods: {
    initData(){
      this.treeSelectList = this.listToTree(this.list, this.props);
      console.log(this.treeSelectList);
    },
    /**
     * @description        数组转树形数据
     * @param {数据数组}    list
     * @param {树结构配置}  config
     */
    listToTree(list, config) {
      let conf = {};
      Object.assign(conf, config);
      const nodeMap = new Map();
      const result = [];
      const { id, children, pid } = conf;
      for(const node of list) {
        // node[children] = node[children] || [];
        nodeMap.set(node[id], node);
      }
      for(const node of list) {
        const parent = nodeMap.get(node[pid]);
        (parent ? (parent.children ? parent.children : parent.children = []) : result).push(node);
      }
      return result;
    },
    // 树形选择器 - 取值
    getValue(value){
      // this.valueId = value
      console.log('getValue', value, this.valueId);
    },
    // 树形选择器 - 取值
    getValue2(value){
      // this.valueId = value
      console.log('getValue2', value, this.valueIds);
    }
  },
  watch:{
    valueId(){
      console.log('valueId', this.valueId);
    },
    valueIds(){
      console.log('valueIds', this.valueIds);
    }
  }

}
</script>


参考:

基于Element-UI的组件改造的树形选择器(树形下拉框)

Element-UI二次封装实现TreeSelect 树形下拉选择组件

element ui的el-tree多选树(复选框)父子节点关联不关联的问题

vue+element-ui之tree树形控件有关子节点和父节点之间的各种选中关系详解

;