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

作者:神秘网友 发布时间:2020-10-24 01:34:28

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

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

最近公司有个新需求,希望能有一个下拉选择树的功能,大概的功能和样式如下所示:
vue造轮子之封装选择下拉树
vue造轮子之封装选择下拉树

然后我的第一反应就是上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>

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

样式和基本结构也已经构建好了,值得注意一点就是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>

vue造轮子之封装选择下拉树
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样式保持一致

源码地址为:https://github.com/whenTheMorningDark/vue-kai-admin/blob/master/src/components/treeSelect/index.vue

vue造轮子之封装选择下拉树相关教程

  1. Vue Element Ui中 Table 表格更改某一列的样式,比如说背景色

    Vue Element Ui中 Table 表格更改某一列的样式,比如说背景色 先在Table 表格 上加 上属性 el-table :data=tableList border :cell-style=columnStyle el-table-column type=index label=第一列 width=50px/el-table-column el-table-column type=index label

  2. Vue通过eventBut实现组件全局通信

    Vue通过eventBut实现组件全局通信 一、组件之间的层级关系如下图: 现要在test_page_1.vue 组件中改变,MyHeader.vue组件中的某个属性值。 二、eventBus简介: EventBus 又称为事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用

  3. RxJS结合vue-rx, Akita的介绍和使用

    RxJS结合vue-rx, Akita的介绍和使用 介绍 : 解决异步事件。它将一切数据包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后利用强大丰富的操作符(Operators )对流进行处理,使得用户可以用同步编程的方式处理异步数据。 基本概念 :Observable 类

  4. vue Element 利用Cascader 级联选择器实现三层选择器

    vue Element 利用Cascader 级联选择器实现三层选择器 在这次项目需求中要实现的功能和选择地址的过程是很相似的,就用选择地址的过程来举例:首先是选择省,然后是市,再是某个区。在实际的开发过程中这种场景是很常见的,所以我就分享一下我的实现过程。 效

  5. nginx部署多个vue项目如何配置

    nginx部署多个vue项目如何配置 使用同一域名或者ip去部署访问多个前端项目,比如域名/ip直接访问官网,域名/ip后面带路径去访问其它项目 官网访问地址: http://192.168.27.119/login项目二访问地址:http://192.168.27.119/biz/login项目三访问地址:http://1

  6. 在 vue 中使用 echarts 的详细步骤

    在 vue 中使用 echarts 的详细步骤 年龄大了,果然脑力是跟不上了,这个图表已经用过几次了,每次还是或多或少的出现问题,现在把 echarts 在 vue 中的详细使用步骤记录下来,以备不时之需吧。 echarts 图表绘制的思路是: 1 获取一个有宽高的 DOM 元素 -- 2

  7. Jenkins + Maven + Github/Gitlab + Springboot/Vue.js 实现自动

    Jenkins + Maven + Github/Gitlab + Springboot/Vue.js 实现自动化部署 Jenkins用户文档地址 Jenkins在docker环境下安装非常简单。只需要执行命令 docker run \ -u root \ --rm \ -d \ -p 8080:8080 \ -p 50000:50000 \ -v jenkins-data:/var/jenkins_home \

  8. VuePress 侧边栏使用详解

    VuePress 侧边栏使用详解 前言 官网写的侧边栏教程真的是让人很糟心,经过一番摸索,总算大致弄清楚了,下面详细说一下用法: 目录结构: docs根目录下有一个README.md、chinese文件夹、english文件夹 最简侧边栏: sidebar: { '/language/chinese/': [ '', /