vue造轮子之封装选择下拉树

1,043 阅读2分钟

最近公司有个新需求,希望能有一个下拉选择树的功能,大概的功能和样式如下所示: 在这里插入图片描述 在这里插入图片描述

然后我的第一反应就是上elementui上找现成的组件,但是挺遗憾的就是element并没有提供这样的组件,所以只能自己动手造一个了。

1. 组件需求 (1) 支持单选和多选功能 (2) 叶子节点控制是否能选择 (3) 数据回显到选择框支持多选和单选显示 (4) 支持树节点搜索功能 (5) 基本样式应与elementUi样式保持一致

2.布局和样式代码编写 由于这个地方需要用到弹出下拉框,所以我就借助了el-popover来实现这个功能,代码如下:

<template>
  <div class="ka-tree-select" :style="{width:width+'px'}">
     <el-popover
        placement="bottom"
        :width="width"
        trigger="click">
    <div class="ka-select-box" slot="reference">
      <div class="tag-box">
        <div v-show="selecteds.length>0">
          显示的内容
        </div>
         <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p>
      </div>
    </div>
  </el-popover>
  </div>
</template>

<script>
export default {
  name: "treeSelect",
  props: {
    width: {
      type: [String, Number],
      default: 200
    }
  },
  data() {
    return {
      selecteds: [] // 选择到的数据
    };
  }
};
</script>

<style lang="scss" scoped>
  .ka-tree-select{
    position: relative;
    display: inline-block;
    width: 100%;
    vertical-align: middle;
    outline: none;
    .ka-select-box {
    display: flex;
    border: 1px solid #dcdfe6;
    padding: 0 5px 0 8px;
    width: 100%;
    min-height: 36px;
    // height: 36px;
    line-height: 34px;
    box-sizing: border-box;
    border-radius: 4px;
    cursor: pointer;
    outline: none;
    &:focus {
      border-color: #409eff;
    }
    > .tag-box {
      display: inline-block;
      width: calc(100% - 20px);
      text-align: left;
    }

    > .icon-box {
      float: right;
      display: flex;
      width: 20px;
      justify-content: center;
      align-items: Center;
      color: #c0c4cc;
      // transform: rotateZ(45deg);
      .el-icon-arrow-down {
        transition: all 0.2s;
      }
      .down {
        transform: rotateZ(180deg);
      }
    }
    .ka-placeholder-box {
      color: #c0c4cc;
      margin: 0;
    }
  }
  }
</style>

在这里插入图片描述

样式和基本结构也已经构建好了,值得注意一点就是HTML结构中,.tag-box这个类名容器中会存放所选择到的数据,是用来存放单选和多选的页签。

3.树结构制作 这里的树结构我主要使用到了el-tree这个组件,为了让它有个好看的滚动条,这里我也选择了el-scrollBar滚动条组件,值得一提的就是使用el-scrollbar组件时候,必须给父容器添加一个高度,然后el-scrollBar的高度为100%,要不然滚动条为出不来。代码如下:

<template>
  <div class="ka-tree-select" :style="{width:width+'px'}">
     <el-popover
        placement="bottom"
        :width="width"
        trigger="click">

    <el-input v-if="filterable" v-model="filterText" :size="size" placeholder="请输入关键词"></el-input>
      <el-scrollbar class="ka-treeselect-popover">
        <el-tree
          :data="data"
          :props="selfProps"

          :node-key="nodeKey"
          highlight-current
          :default-checked-keys="checked_keys"
          :default-expanded-keys="expandedKeys"
          :show-checkbox="checkbox"
          check-strictly
          ref="tree-select"
          :filter-node-method="filterNode"
        ></el-tree>
        <!-- @check="handleCheckChange" -->
        <!-- @node-click="handleNodeClick" -->
      </el-scrollbar>

    <div class="ka-select-box" slot="reference">
      <div class="tag-box">
        <div v-show="selecteds.length>0">
          显示的内容
        </div>
         <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p>
      </div>
    </div>
  </el-popover>
  </div>
</template>

<script>
export default {
  name: "treeSelect",
  props: {
    width: { // 宽度
      type: [String, Number],
      default: 200
    },
    size: { // 尺寸
      type: String,
      default: "mini"
    },
    data: { // 树结构的数据
      type: Array,
      default: () => []
    },
    nodeKey: { // 树结构的唯一标识
      type: String,
      default: "id"
    },
    // 是否使用搜索
    filterable: {
      type: Boolean,
      default: true
    },
    // 显示的字段
    props: {
      type: Object,
      default: () => ({
        label: "label",
        children: "children",
      })
    },
    // 是否可多选
    checkbox: {
      type: Boolean,
      default: false
    },
  },
  data() {
    return {
      selecteds: [], // 选择到的数据
      checked_keys: [], // 默认选中的数据
      expandedKeys: [], // 默认展开的数据
      filterText: "" // 筛选的数据
    };
  },
  computed: {
    selfProps () {
      return {
        label: "label",
        children: "children",
        disabled: data => data.disabled,
        ...this.props
      };
    },
  },
  methods: {
    // 树节点筛选
    filterNode (value, data) {
      if (!value) {
        return true;
      }
      return data[this.selfProps.label].indexOf(value) !== -1;
    }
  },
  watch: {
    filterText (val) {
      this.$refs["tree-select"].filter(val);
    }
  }
};
</script>

<style lang="scss" scoped>
  .ka-tree-select{
    position: relative;
    display: inline-block;
    width: 100%;
    vertical-align: middle;
    outline: none;
    .ka-select-box {
    display: flex;
    border: 1px solid #dcdfe6;
    padding: 0 5px 0 8px;
    width: 100%;
    min-height: 36px;
    // height: 36px;
    line-height: 34px;
    box-sizing: border-box;
    border-radius: 4px;
    cursor: pointer;
    outline: none;
    &:focus {
      border-color: #409eff;
    }
    > .tag-box {
      display: inline-block;
      width: calc(100% - 20px);
      text-align: left;
    }

    > .icon-box {
      float: right;
      display: flex;
      width: 20px;
      justify-content: center;
      align-items: Center;
      color: #c0c4cc;
      // transform: rotateZ(45deg);
      .el-icon-arrow-down {
        transition: all 0.2s;
      }
      .down {
        transform: rotateZ(180deg);
      }
    }
    .ka-placeholder-box {
      color: #c0c4cc;
      margin: 0;
    }
  }
  }
  .ka-treeselect-popover {
  height: 360px;
  /deep/ .el-scrollbar__wrap {
    overflow-x: hidden;
  }
}
</style>

在这里插入图片描述 4.处理显示的值 接下来的话就轮到控制显示输入框的值了,我们要考虑一点,作为公共组件是如何提供给业务组件使用的问题,我假设在业务组件A中,使用了这个值,并且期望它的使用方式如下:

<template>
  <div style="padding:10px">
    <el-row>
      <el-col :span="12">
        <treeSelect :data="treeData" v-model="select" checkbox :width="500" ref="treeSelect" ></treeSelect>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import treeSelect from "@/components/treeSelect/test";
export default {
  components: {
    treeSelect
  },
  data () {
    return {
      treeData: [{
        id: 1,
        label: "一级 1",
        children: [{
          id: 4,
          label: "二级 1-1",
          children: [{
            id: 9,
            label: "三级 1-1-1"
          }, {
            id: 10,
            label: "三级 1-1-2"
          }]
        }]
      }, {
        id: 2,
        label: "一级 2",
        children: [{
          id: 5,
          label: "二级 2-1"
        }, {
          id: 6,
          label: "二级 2-2"
        }]
      }, {
        id: 3,
        label: "一级 3",
        children: [{
          id: 7,
          label: "二级 3-1"
        }, {
          id: 8,
          label: "二级 3-2"
        }, {
          id: 18,
          label: "二级 3-2"
        }, {
          id: 82,
          label: "二级 3-2"
        }, {
          id: 84,
          label: "二级 3-2"
        }, {
          id: 842,
          label: "二级 3-2"
        }, {
          id: 847,
          label: "二级 3-2"
        }]
      },
      {
        id: 11,
        label: "最外面"
      }
      ],
      select: [9, 5]
    };
  }
};
</script>

<style>
</style>

select这个变量代表我选中了这两个节点,还可以默认展开,并且是在输入框中显示所对应的label值,那么就回到treeSelect组件中编写对应的逻辑。在created中定义处理默认值的方法handDefaultValue

 // 处理默认值的方法
    handDefaultValue(value) {
      if (!Array.isArray(value) || value.length === 0) {
        return;
      }
      this.expandedKeys = [];
      if (!this.checkbox) { // 单选的情况
        this.$nextTick(() => {
          this.$refs["tree-select"].setCurrentNode({
            id: value[0]
          });
          let currentNode = this.$refs["tree-select"].getCurrentNode();
          this.expandedKeys.push(value[0]);
          this.selecteds = [currentNode];
          this.$emit("change", this.selecteds);
        });
      } else { // 多选的情况
        this.$nextTick(() => {
          this.$refs["tree-select"].setCheckedKeys(value);
          let currentAllNode = this.$refs["tree-select"].getCheckedNodes();
          value.forEach(v => {
            this.expandedKeys.push(v);
          });
          this.selecteds = currentAllNode;
          this.$emit("change", this.selecteds);
        });
      }
    }

这个时候我们的selecteds数组中已经初始化了选中当前树节点的数据,这个时候我们只需要把树节点对应的label渲染出来即可,这里我使用到el-tag这个组件,我把文字的显示分成了两种情况一种是平铺展示,一种折叠显示,代码如下:

<template v-if="!collapseTags">
              <el-tag
                closable
                :size="size"
                v-for="item in selecteds"
                :title="item[selfProps.label]"
                :key="item[nodeKey]"
                class="ka-select-tag"
                @close="tabClose(item[nodeKey])"
              >{{ item[selfProps.label] }}</el-tag>
            </template>
            <template v-else>
              <el-tag
                closable
                :size="size"
                class="ka-select-tag"
                :title="collapseTagsItem[selfProps.label]"
                @close="tabClose(collapseTagsItem[nodeKey])"
              >{{ collapseTagsItem[selfProps.label] }}</el-tag>
              <el-tag
                v-if="this.selecteds.length>1"
                :size="size"
                class="ka-select-tag"
              >+{{ this.selecteds.length-1}}</el-tag>
            </template>

此时完整的代码:

<template>
  <div class="ka-tree-select" :style="{width:width+'px'}">
     <el-popover
        placement="bottom"
        :width="width"
        trigger="click">

    <el-input v-if="filterable" v-model="filterText" :size="size" placeholder="请输入关键词"></el-input>
      <el-scrollbar class="ka-treeselect-popover">
        <el-tree
          :data="data"
          :props="selfProps"

          :node-key="nodeKey"
          highlight-current
          :default-checked-keys="checked_keys"
          :default-expanded-keys="expandedKeys"
          :show-checkbox="checkbox"
          check-strictly
          ref="tree-select"
          :filter-node-method="filterNode"
        ></el-tree>
        <!-- @check="handleCheckChange" -->
        <!-- @node-click="handleNodeClick" -->
      </el-scrollbar>

    <div class="ka-select-box" slot="reference">
      <div class="tag-box">
        <div v-show="selecteds.length>0">
          <template v-if="!collapseTags">
              <el-tag
                closable
                :size="size"
                v-for="item in selecteds"
                :title="item[selfProps.label]"
                :key="item[nodeKey]"
                class="ka-select-tag"
                @close="tabClose(item[nodeKey])"
              >{{ item[selfProps.label] }}</el-tag>
            </template>
            <template v-else>
              <el-tag
                closable
                :size="size"
                class="ka-select-tag"
                :title="collapseTagsItem[selfProps.label]"
                @close="tabClose(collapseTagsItem[nodeKey])"
              >{{ collapseTagsItem[selfProps.label] }}</el-tag>
              <el-tag
                v-if="this.selecteds.length>1"
                :size="size"
                class="ka-select-tag"
              >+{{ this.selecteds.length-1}}</el-tag>
            </template>
        </div>
         <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p>
      </div>
    </div>
  </el-popover>
  </div>
</template>

<script>
export default {
  name: "treeSelect",
  props: {
    width: { // 宽度
      type: [String, Number],
      default: 200
    },
    size: { // 尺寸
      type: String,
      default: "mini"
    },
    data: { // 树结构的数据
      type: Array,
      default: () => []
    },
    nodeKey: { // 树结构的唯一标识
      type: String,
      default: "id"
    },
    // 是否使用搜索
    filterable: {
      type: Boolean,
      default: true
    },
    // 显示的字段
    props: {
      type: Object,
      default: () => ({
        label: "label",
        children: "children",
      })
    },
    // 是否可多选
    checkbox: {
      type: Boolean,
      default: false
    },
    // 选中数据
    value: [Array],
    // 多选时是否将选中值按文字的形式展示
    collapseTags: {
      type: Boolean,
      default: false
    },
  },
  created () {
    this.handDefaultValue(this.value);
  },
  data() {
    return {
      selecteds: [], // 选择到的数据
      checked_keys: [], // 默认选中的数据
      expandedKeys: [], // 默认展开的数据
      filterText: "" // 筛选的数据
    };
  },
  computed: {
    selfProps () {
      return {
        label: "label",
        children: "children",
        disabled: data => data.disabled,
        ...this.props
      };
    },
  },
  methods: {
    // 树节点筛选
    filterNode (value, data) {
      if (!value) {
        return true;
      }
      return data[this.selfProps.label].indexOf(value) !== -1;
    },
    // 处理默认值的方法
    handDefaultValue(value) {
      if (!Array.isArray(value) || value.length === 0) {
        return;
      }
      this.expandedKeys = [];
      if (!this.checkbox) { // 单选的情况
        this.$nextTick(() => {
          this.$refs["tree-select"].setCurrentNode({
            id: value[0]
          });
          let currentNode = this.$refs["tree-select"].getCurrentNode();
          this.expandedKeys.push(value[0]);
          this.selecteds = [currentNode];
          this.$emit("change", this.selecteds);
        });
      } else { // 多选的情况
        this.$nextTick(() => {
          this.$refs["tree-select"].setCheckedKeys(value);
          let currentAllNode = this.$refs["tree-select"].getCheckedNodes();
          value.forEach(v => {
            this.expandedKeys.push(v);
          });
          this.selecteds = currentAllNode;
          this.$emit("change", this.selecteds);
        });
      }
    }
  },
  watch: {
    filterText (val) {
      this.$refs["tree-select"].filter(val);
    }
  }
};
</script>

<style lang="scss" scoped>
  .ka-tree-select{
    position: relative;
    display: inline-block;
    width: 100%;
    vertical-align: middle;
    outline: none;
    .ka-select-box {
    display: flex;
    border: 1px solid #dcdfe6;
    padding: 0 5px 0 8px;
    width: 100%;
    min-height: 36px;
    // height: 36px;
    line-height: 34px;
    box-sizing: border-box;
    border-radius: 4px;
    cursor: pointer;
    outline: none;
    &:focus {
      border-color: #409eff;
    }
    > .tag-box {
      display: inline-block;
      width: calc(100% - 20px);
      text-align: left;
    }

    > .icon-box {
      float: right;
      display: flex;
      width: 20px;
      justify-content: center;
      align-items: Center;
      color: #c0c4cc;
      // transform: rotateZ(45deg);
      .el-icon-arrow-down {
        transition: all 0.2s;
      }
      .down {
        transform: rotateZ(180deg);
      }
    }
    .ka-placeholder-box {
      color: #c0c4cc;
      margin: 0;
    }
    .ka-select-tag {
      max-width: 100%;
      text-overflow: ellipsis;
      overflow: hidden;
      word-wrap: break-word;
      word-break: break-all;
      vertical-align: middle;
    }
    .ka-select-tag + .ka-select-tag {
      margin-left: 4px;
    }
  }
  }
  .ka-treeselect-popover {
  height: 360px;
  /deep/ .el-scrollbar__wrap {
    overflow-x: hidden;
  }
}
</style>

这个时候只需关注点击树节点所触发的函数,点击树节点el-checkBox和关闭el-tag时的函数即可。

// 点击checkbox变化
    handleCheckChange (val) {
      let nodes = this.$refs["tree-select"].getCheckedNodes(false);
      this.selecteds = nodes;
      this.$emit("change", this.selecteds);
    },
    // 点击列表树
    handleNodeClick (item, node) {
      if (this.checkbox) {
        return;
      }
      this.selecteds = [item];
      // this.options_show = false;
      this.$emit("change", this.selecteds);
    },
    // tag标签关闭
    tabClose (id) {
      if (this.disabled) {
        return;
      }
      if (!this.checkbox) { // 单选
        this.selecteds = [];
        this.$refs["tree-select"].setCurrentKey(null);
      } else { // 多选
        this.$refs["tree-select"].setChecked(id, false, true);
        this.selecteds = this.$refs["tree-select"].getCheckedNodes();
      }
      this.$emit("change", this.selecteds);
    },

那么最后还有最后一个小的功能点,就是实现只能选择叶子节点的功能,那么我们就定义一个变量isLeaf来进行标识。在el-tree中只需要给节点添加上disabled字段即可标识当前节点是否可选了,那么我们只需要遍历这个树节点,循坏遍历,判断当前节点的children字段是否是大于0,大于0,disable则为true。

// 转换treedata
    changeTreeData (data) {
      if (!data) {
        return;
      }
      let stack = [];
      data.forEach(v => {
        stack.push(v);
      });
      while (stack.length) {
        const result = stack.shift();
        if (result.children && result.children.length > 0) {
          result.disabled = true;
          stack = stack.concat(result.children);
        } else {
          result.disabled = false;
        }
      }
      return data;
    },
created(){
	this.isLeaf && this.changeTreeData(this.data);
}

这个时候已经完成了下拉选择树的功能,功能要点如下: (1) 支持单选和多选功能 (2) 叶子节点控制是否能选择 (3) 数据回显到选择框支持多选和单选显示 (4) 支持树节点搜索功能 (5) 基本样式应与elementUi样式保持一致

源码地址为:github.com/whenTheMorn…