基于iview的树形选择器

树形选择器 基于iview

实现: 多级树形结构,可搜索,可清空
在这里插入图片描述

传入data结构:

data: [
  {
    title: '早餐',
    children: [
      {
        title: '北方',
        children: [
          {
            title: '豆浆',
            children: [
              {title: '豆浆+咸豆腐脑'}
            ]
          },
          {
            title: '油条',
            children: [
              {title: '油条+胡辣汤'}
            ]
          }
        ]
      },
      {
        title: '南方',
        children: [
          {
            title: '豆浆',
            children: [
              {title: '豆浆+甜豆腐脑'}
            ]
          },
          {title: '肠粉'},
          {title: '虾饺'}
        ]
      }
    ]
  },
  {
    title: '午餐',
    children: [
      {
        title: '北方',
        children: [
          {title: '面条'},
          {title: '饺子'}
        ]
      },
      {
        title: '南方',
        children: [
          {title: '米饭'},
          {title: '馄饨'}
        ]
      }
    ]
  }
],

代码:

<template>
  <div>
    <span class="tech-sel-wrapper">
      <div
        ref="selectBox"
        :class="['select-box', {active: showClass.isActive}]"
        tabindex="-1"
        @blur="blur"
        @focus="focus"
        @mouseenter="hover"
        @mouseleave="() => {this.showClear = false}"
        :style="{width: `${width}px`}">
        <div class="tag-box">
          <template v-for="(item, index) in selectNodes">
            <!-- Tag标签 
              fade:是否在出现和消失时使用渐变的动画,动画时长可能会引起占位的闪烁
              closable:标签是否可以关闭
              name:当前标签的名称,使用 v-for,并支持关闭时,会比较有用
              on-close:关闭时触发 
            -->
            <Tag
              :fade="false"
              closable
              :key="index"
              @on-close="closeTag"
              :name="JSON.stringify(item)">
              {{item.title}}
            </Tag>
          </template>
        </div>
        <div v-if="clearable" class="clear-btn" v-show="showClear" @click="clear">
          <Icon type="md-close-circle" size="20"></Icon>
        </div>
        <div :class="['content-box', {'fade-in': showClass.fadeIn}, {'fade-out': showClass.fadeIn}]" ref="contentBox">
          <div class="input-box" v-if="searchable" ref="inputBox">
            <div style="width: 40%;display: inline-block">
              <!-- Input输入框 
                icon:输入框尾部图标,仅在 text 类型下有效;type 默认 text
                on-blur:输入框失去焦点时触发
                on-keyup:原生的 keyup 事件,按钮被松开
              -->
              <Input
                @on-keyup="keyUp"
                @on-blur="inputBlur"
                v-model="keyWord"
                size="small"
                icon="ios-search">
              </Input>
            </div>
          </div>
          <div class="tree-box" ref="treeBox">
            <Tree
              :data="searching ? searchData : originData"
              multiple
              show-checkbox
              @on-check-change="setSelectNodes"
            ></Tree>
          </div>
        </div>
      </div>
    </span>
  </div>
</template>

<script>
  const contains = (parentNode, childNode) => {
    // Node.contains()方法返回一个Boolean值,
    // 该值指示节点是否是给定节点的后代,即节点本身、其直接子节点 ( childNodes) 之一、子节点的直接子节点之一,等等。
    if (parentNode && childNode) return parentNode.contains(childNode)
    return false
  }

  export default {
    props: {
      data: { // 树形结构的数据
        type: Array,
        default: null,
        required: true
      },
      searchable: { // 是否搜索,设置该属性为true时,可以根据子节点的title进行搜索
        type: Boolean,
        default: false,
        required: false
      },
      clearable: { // 是否清除已选项
        type: Boolean,
        default: false,
        required: false
      },
      width: { // 选择框的宽度
        type: [String, Number],
        default: 300,
        required: false
      },
      value: { // v-model绑定的数据
        type: Array,
        default: null,
        required: true
      },
      pkey: { // 设置每一个节点的唯一标识
        type: String,
        default: 'title',
        required: false
      }
    },
    data () {
      return {
        searching: false, // 为false是originData,为true是searchData
        searchData: [],   // 搜索的数据
        originData: [],   // this.data加上value属性的数据
        selectNodes: [],  // 选中的数据
        keyWord: null,    // 搜索的关键词
        showClear: false, // 清除按钮是否显示
        showClass: {
          'isActive': false,
          'fadeIn': false,
          'fadeOut': false
        }
      }
    },
    methods: {
      focus() { // 获得焦点
        this.showClass.isActive = true; // active 边框加颜色,把一个或多个下拉阴影添加到框上
        this.showClass.fadeIn = true; // 不透明度,从0.0(完全透明)到1.0(完全不透明)
      },
      blur({relatedTarget}) { // 失去焦点 
        // relatedTarget --- 选中的节点 --- <input type="checkbox" class="ivu-checkbox-input">
        // this.$refs.inputBox --- 搜索框   --- <div data-v-a7dae8d6 class="input-box">...</div>
        // this.$refs.treeBox  --- 树形结构 --- <div data-v-a7dae8d6 class="tree-box">...</div>
        switch (true) {
          case contains(this.$refs.inputBox, relatedTarget):
            break;
          case contains(this.$refs.treeBox, relatedTarget):
            this.$refs.selectBox.focus();
            break;
          default:
            this.showClass.isActive = false;
            this.showClass.fadeIn = false;
            this.showClass.fadeOut = true;
        }
      },
      /** hover() 方法规定当鼠标指针悬停在被选元素上时要运行的两个函数。
      方法触发 mouseenter 和 mouseleave 事件。
      注意: 如果只指定一个函数,则 mouseenter 和 mouseleave 都执行它
      **/
      hover() {  // 鼠标经过
        if (this.selectNodes.length > 0) this.showClear = true
      },
      // input输入框失去焦点
      inputBlur({relatedTarget}) {  
        // relatedTarget --- 选中的节点 --- <input type="checkbox" class="ivu-checkbox-input">
        // this.$refs.selectBox --- 整个div  --- <div data-v-a7dae8d6 tabindex="-1" class="select-box" style="width: 300px;">...</div>
        // this.$refs.treeBox   --- 树形结构 --- <div data-v-a7dae8d6 class="tree-box">...</div>
        switch (true) {
          case relatedTarget === this.$refs.selectBox:
            this.$refs.selectBox.focus();
            break;
          case contains(this.$refs.treeBox, relatedTarget):
            this.$refs.selectBox.focus();
            break;
          default:
            this.showClass.isActive = false;
            this.showClass.fadeIn = false;
            this.showClass.fadeOut = true;
        }
      },
      // 清除已选项
      clear() {
        this.selectNodes = [];
        this.originData = this.resetTree(
          (node) => {
            delete node.checked
            delete node.indeterminate
          }
        )
        this.setSelectNodes();
      },
      setSelectNodes() {
        let nodes = []; // 选中的数据
        this.traverseTree(
          {children: this.originData},
          (node) => {
            if (!node.children && node.checked === true) nodes.push(node)
          }
        )
        this.$emit('input', this.selectNodes = nodes);
      },
      // input输入框按钮被松开
      keyUp() {   
        if (this.keyWord && this.keyWord.length > 0) { // 有搜索关键词时用searchData
          this.searchData = [];
          this.traverseTree(
            {children: this.originData},
            (node) => {
              if (!node.children && node.title.includes(this.keyWord)) this.searchData.push(node)
            }
          )
          this.searching = true;
        } else {
          // 无搜索关键词时用originData,尽可能还原originData,如果node有children则删除node的checked与indeterminate属性
          // 在按搜索词选中后再删除搜索词,this.originData选中项的上级刚开始并没有checked与indeterminate属性,在经过this.resetTree才有????
          this.originData = this.resetTree( 
            (node) => {
              if (node.children && node.children.length > 0) {
                delete node.checked
                delete node.indeterminate
              }
            }
          )
          this.searching = false;
        }
      },
      // 删除tag标签
      closeTag(event, value) {  
        // value: {"title":"豆浆","value":"早餐/北方/豆浆","nodeKey":2,"checked":true,"indeterminate":false} 
        let curKey = JSON.parse(value)[this.pkey]; // 豆浆
        this.originData = this.resetTree(
          (node) => {
            if (node.children && node.children.length > 0) {
              delete node.checked
              delete node.indeterminate
            } else if (node[this.pkey] === curKey) node.checked = false
          }
        )
        this.setSelectNodes();
      },
      traverseTree(node, callBack, parentNode) {
        // 确认callBack有值,并执行callBack函数
        callBack && callBack(node, parentNode)
        if (node.children && node.children.length > 0) {
          for (let index in node.children) {
            this.traverseTree(node.children[index], callBack, node);
          }
        }
      },
      resetTree(callBack) {
        let cloneNode = JSON.parse(JSON.stringify(this.originData.length > 0 ? this.originData : this.data));
        this.traverseTree(
          {children: cloneNode},
          callBack
        )
        return cloneNode;
      }
    },
    created () {
      // this.value = [{checked: true, indeterminate: false, nodeKey: 0, title: "豆浆", value: "早餐/北方/豆浆"},
      // {checked: true, indeterminate: false, nodeKey: 3, title: "油条", value: "早餐/北方/油条"}]
      
      // this.pkey = 'title'
      // keys 记录value里面所有pkey值 ["豆浆", "油条"]
      let keys = this.value.map((val) => {
        if (val[this.pkey]) return val[this.pkey]
      })
      this.originData = this.resetTree(
        (node, parentNode) => {
          if (parentNode && parentNode[this.pkey]) {
            node.value = `${parentNode.value}/${node[this.pkey]}`;
          } else {
            node.value = node[this.pkey];
          }
          // includes() 方法用来判断一个数组keys是否包含一个指定的值node[this.pkey],如果是返回 true,否则false
          if (!node.children && keys.includes(node[this.pkey])) node.checked = true
        }
      )
      this.setSelectNodes();
    }
  }
</script>

<style scoped lang="scss">
  .tech-sel-wrapper {
    display: inline-block;
    text-align: left;
    .select-box {
      position: relative;
      min-height: 40px;
      border-radius: 5px;
      border: 1px solid #CCC;
      transition: .3s;
      .clear-btn {
        position: absolute;
        right: 10px;
        top: 50%;
        transform: translateY(-50%); // 垂直居中
        transition: .5s;
        cursor: pointer;
        &:hover {
          color: #f07649;
        }
      }
      .tag-box {
        width: 100%;
        padding: 5px;
        max-height: 300px;
        overflow-y: auto;
      }
      &:hover {
        border-color: #57a3f3;
      }
      &:focus, &.active {
        border-color: #57a3f3;
        // 把一个或多个下拉阴影添加到框上 
        // box-shadow: h-shadow水平 v-shadow垂直 blur模糊距离 spread阴影大小 color阴影颜色 inset从外层的阴影(开始时)改变阴影内侧阴影;
        box-shadow: 0 0 0 2px rgba(45, 140, 240, .2); 
        outline: none;
      }
      .content-box {
        height: 0;
        background-color: #FFF;
        position: absolute;
        z-index: 100;
        left: 0;
        top: 100%;
        margin-top: 5px;
        padding-left: 10px;
        box-shadow: rgba(0, 0, 0, 0.15) 0 2px 8px 0;
        width: 100%;
        max-height: 400px;
        overflow-y: auto;
        &.fade-out {
          animation: fade-out .5s forwards;
        }
        &.fade-in {
          animation: fade-in .3s forwards;
        }
        .input-box {
          padding-top: 5px;
          padding-right: 10px;
          text-align: right;
        }
      }
    }
  }
  // 不透明度,从0.0(完全透明)到1.0(完全不透明)
  @keyframes fade-in {
    0% {
      height: auto;
      opacity: 0;
    }
    100% {
      height: auto;
      opacity: 1;
    }
  }
  // 不透明度,从1.0(完全不透明)到 0.0(完全透明),高度从自动到0
  @keyframes fade-out {
    0% {
      height: auto;
      opacity: 1;
    }
    50% {
      opacity: 0;
    }
    100% {
      height: 0;
    }
  }
</style>

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值