VUE2,基于 el-tree 组件基础上封装的 省市区/县 选择器

最近在写一个vue2中后台项目需要用到 省市区 区域选择组件,组件需支持如下功能:

  1. 可支持多选
  2. 省、市、区/县 均可支持选择
  3. 当前省选中时默认下面的市、县/区默认选中,当前市选中时同理
  4. 当选中省时只需返回当前省的那一项数据,下面市县/区不需要返回,选中市时同理
但是找了下没有很符合这些需求的vue2组件,于是自己封装了这个组件
除上述功能之外该组件还支持双向绑定和两种数据格式
效果图

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

禁用时效果图

在这里插入图片描述

完整代码如下
<template>
  <div v-clickoutside="closeTree">
    <div @click="inputFocus">
      <div
        :style="'width:' + width + 'px'"
        :class="'box ' + (tabData.length ? 'arrow':'') + (ishowTree ? ' box-blue' : '') + (disabled ? ' disabled' : '')"
      >
        <div v-for="item in tabData" :key="item[keys]" class="box-item">
          <div class="box-item-name">{{ item.name }}</div>
          <i v-if="!disabled" class="el-icon-close" @click.stop="handleDelArea(item.id)" />
        </div>
        <i v-if="tabData.length && !disabled" class="el-icon-error" @click.stop="handleDelAllArea()" />
        <div v-if="!tabData.length" class="box-placeholder">{{ placeholder }}</div>
        <div class="arrow-box">
          <i :class="(ishowTree ? 'arrow-up' : 'arrow-down') + ' el-icon-arrow-down'" />
        </div>
      </div>
    </div>
    <div class="treeModule">
      <el-tree
        ref="tree"
        key="areaCode"
        :class="ishowTree ? 'ORGTree' : 'ORGTree-close'"
        :style="'width:' + width + 'px'"
        :data="data"
        node-key="areaCode"
        highlight-current
        :props="defaultProps"
        :default-checked-keys="checkKeys"
        show-checkbox
        @check="handleCheckChange"
      />
    </div>
  </div>
</template>

<script>
import Clickoutside from 'element-ui/src/utils/clickoutside'
export default {
  directives: { Clickoutside },
  props: {
    value: {
      type: Array,
      default: () => {
        return []
      }
    }, // 值
    valueType: {
      type: String,
      default: 'complex'
    }, // 值类型 simple: ['11','22'],complex: [{areaCode: 11, areaName:'北京市', parentAreaCode:'', ...}]
    width: {
      type: Number,
      default: 600
    }, // 宽度
    keys: {
      type: String,
      default: 'areaCode'
    }, // key值对应的属性名
    label: {
      type: String,
      default: 'areaName'
    }, // label值对应的属性名
    parentKeys: {
      type: String,
      default: 'parentAreaCode'
    }, // 每一项对应的父级的key
    placeholder: {
      type: String,
      default: '请选择'
    }, // 缺省占位文本
    proIdLen: {
      type: Number,
      default: 2
    }, // 省级 id 长度
    cityIdLen: {
      type: Number,
      default: 4
    }, // 市级 id 长度
    disabled: {
      type: Boolean,
      default: false
    }, // 是否禁用
    data: {
      type: Array,
      default: () => {
        return []
      }
    } // 省市区/县数据
  },
  data() {
    return {
      defaultProps: {
        children: 'children',
        label: 'areaName'
      },
      checkKeys: [], // 选中的key
      areaItems: [], // 选中项的完整数据
      tabData: [], // 展示的选中项
      ishowTree: false
    }
  },
  watch: {
    // 深度监听 value 值的变化
    value: {
      handler() {
        if (this.valueType === 'complex') {
          const keys = []
          for (const i in this.value) {
            keys.push(this.value[i].areaCode)
          }
          if (JSON.stringify(keys) !== JSON.stringify(this.checkKeys)) {
            this.init(keys)
          }
        } else {
          if (JSON.stringify(this.value) !== JSON.stringify(this.checkKeys)) {
            this.init(this.value)
          }
        }
      },
      deep: true
    }
  },
  methods: {
    // 初始化
    init(keys) {
      this.$refs.tree.setCheckedKeys(keys)
      this.handleCheckChange()
    },
    // 选择/取消选择
    async handleCheckChange() {
      const checkKeys = this.$refs.tree.getCheckedNodes()
      this.checkKeys = await this.getCurrentId(checkKeys)
      const currentName = await this.getCurrentName()
      const tabs = []
      for (const i in this.checkKeys) {
        const tabItem = {
          id: this.checkKeys[i],
          name: currentName[i]
        }
        tabs.push(tabItem)
      }
      this.tabData = tabs
      this.echoData()
    },
    inputFocus() {
      if (this.disabled) {
        this.ishowTree = false
        return
      }
      if (this.ishowTree) {
        this.ishowTree = false
      } else {
        this.ishowTree = true
      }
    },
    // 关闭树形控件
    closeTree() {
      this.ishowTree = false
    },
    // 提取区域id
    getCurrentId(ids) {
      const proIds = [] // 省
      let citIds = [] // 市
      let areIds = [] // 区
      for (const i in ids) {
        if (ids[i][this.keys].length === this.proIdLen) {
          proIds.push(ids[i])
        } else if (ids[i][this.keys].length === this.cityIdLen) {
          citIds.push(ids[i])
        } else {
          areIds.push(ids[i])
        }
      }
      // 过滤 有省级的过滤掉下面的市县/区,无省级有市级过滤掉下面的县、区
      for (var i = 0; i < citIds.length; i++) {
        const newIds = areIds.filter(item => item[this.parentKeys] !== citIds[i][this.keys])
        areIds = newIds
      }
      for (var j = 0; j < proIds.length; j++) {
        const newIds = citIds.filter(item => item[this.parentKeys] !== proIds[j][this.keys])
        citIds = newIds
      }
      const allIds = []
      for (const i in proIds) {
        allIds.push(proIds[i].areaCode)
      }
      for (const i in citIds) {
        allIds.push(citIds[i].areaCode)
      }
      for (const i in areIds) {
        allIds.push(areIds[i].areaCode)
      }
      return allIds
    },
    // 根据id获取名称
    getCurrentName() {
      this.areaItems = []
      const currentName = []
      for (const i in this.checkKeys) {
        // 省
        if (this.checkKeys[i].length === this.proIdLen) {
          for (const j in this.data) {
            if (this.data[j][this.keys] === this.checkKeys[i]) {
              currentName.push(this.data[j][this.label])
              this.areaItems.push(this.data[j])
              break
            }
          }
        } else if (this.checkKeys[i].length === this.cityIdLen) { // 市
          for (const j in this.data) {
            let cityName = ''
            for (const k in this.data[j].children) {
              if (this.data[j].children[k][this.keys] === this.checkKeys[i]) {
                cityName = this.data[j][this.label] + this.data[j].children[k][this.label]
                this.areaItems.push(this.data[j].children[k])
                currentName.push(cityName)
              }
            }
            if (cityName) break
          }
        } else { // 县
          for (const j in this.data) {
            let cityName = ''
            for (const k in this.data[j].children) {
              for (const l in this.data[j].children[k].children) {
                if (this.data[j].children[k].children[l][this.keys] === this.checkKeys[i]) {
                  cityName = this.data[j][this.label] +
                    this.data[j].children[k][this.label] +
                    this.data[j].children[k].children[l][this.label]
                  this.areaItems.push(this.data[j].children[k].children[l])
                  currentName.push(cityName)
                }
                if (cityName) break
              }
              if (cityName) break
            }
            if (cityName) break
          }
        }
      }
      return currentName
    },
    // 删除某个选中项
    handleDelArea(id) {
      for (const i in this.tabData) {
        if (this.tabData[i].id === id) {
          this.tabData.splice(i, 1)
          this.checkKeys.splice(i, 1)
        }
      }
      for (const i in this.areaItems) {
        if (this.areaItems[i][this.keys] === id) {
          this.areaItems.splice(i, 1)
        }
      }
      this.$refs.tree.setCheckedKeys(this.checkKeys)
      this.echoData()
    },
    // 删除全部选中项
    handleDelAllArea() {
      this.tabData = []
      this.checkKeys = []
      this.areaItems = []
      this.$refs.tree.setCheckedKeys(this.checkKeys)
      this.echoData()
    },
    // 返回选中的area
    echoData() {
      if (this.valueType === 'complex') {
        const list = JSON.parse(JSON.stringify(this.areaItems))
        for (const i in list) {
          list[i].children = []
          delete list[i].children
        }
        this.$emit('input', list)
      } else {
        this.$emit('input', this.checkKeys)
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.treeModule{
  position: absolute;
	z-index: 999999;
}

.ORGTree{
    width: 298px;
    height: 300px;
    overflow: auto;
    border:1px solid #e4e0e0;
    border-top: 0px;
    box-shadow: rgb(214, 212, 212) 0px 0px 5px;
    transition: all 0.1s;
}
.ORGTree-close{
  height: 0px;
  overflow: hidden;
  transition: all 0.1s;
}
>>> .el-input__inner{
  background-color: #fff !important;
  cursor: pointer !important;
}
.box{
  min-height: 41px;
  width: 600px;
  border: 1px solid #dfe4ed;
  padding: 0 20px;
  display: flex;
  flex-wrap: wrap;
  padding-top: 5px;
  padding-left: 10px;
  position: relative;
  border-radius: 4px;
  transition: border 0.2s;
  cursor: pointer;
  .box-item{
    display: flex;
    min-height: 28px;
    border-radius: 14px;
    background: #f5f5f5;
    line-height: 28px;
    align-items: center;
    padding: 0 10px;
    margin-right: 15px;
    margin-bottom: 5px;
    cursor: auto;
    .box-item-name{
      cursor: text;
    }
    .el-icon-close{
      margin-left: 6px;
      cursor: pointer;
      transition: all 0.2s;
    }
    .el-icon-close:hover{
      color: #1890ff;
      transition: all 0.2s;
    }
  }
  .el-icon-error{
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 10px;
    font-size: 16px;
    opacity: 0;
    cursor: pointer;
    z-index: 10;
  }
  .box-placeholder{
    padding-left: 5px;
    line-height: 32px;
    color: rgb(194, 191, 191);
    z-index: 0;
  }
  .arrow-box{
    top: 50%;
    transform: translateY(-50%);
    right: 10px;
    position: absolute;
    .el-icon-arrow-down{
      font-size: 16px;
      color: rgb(192, 190, 190);
      opacity: 1;
    }
    .arrow-up{
      transform: rotate(-180deg);
      transition: all 0.3s;
    }
    .arrow-down{
      transform: rotate(0deg);
      transition: all 0.3s;
    }
  }

}
.disabled{
  background: #F5F7FA;
  cursor: not-allowed;
  .box-item{
    border: 1px solid #d1d1d3;
    .box-item-name{
      color: #777;
    }
  }
}

.box:hover{
  border: 1px solid #c6c6c7;
  transition: border 0.2s;
  .el-icon-error{
    opacity: 1;
    transition: all 0.2s;
  }
  .el-icon-error:hover{
    color: #1296db;
    transition: all 0.2s;
  }
}

.arrow:hover{
  .el-icon-arrow-down{
    opacity: 0;
    transition: all 0.2s;
  }
}
.disabled:hover{
  border: 1px solid #dfe4ed;
  .el-icon-arrow-down{
    opacity: 1;
  }
}
.box-blue{
  border: 1px solid #1296db;
  transition: border 0.2s;
  border-radius: 4px 4px 0 0 ;
}
.box-blue:hover{
  border: 1px solid #1296db;
}
/*滚动条样式*/
.ORGTree::-webkit-scrollbar {/*滚动条整体样式*/
  width: 6px;     /*高宽分别对应横竖滚动条的尺寸*/
  height: 4px;
}
.ORGTree::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
  border-radius: 5px;
  /* -webkit-box-shadow: inset 0 0 5px rgba(49, 49, 49, 0.2); */
  background: rgba(0,0,0,0.2);
}
.ORGTree::-webkit-scrollbar-track {/*滚动条里面轨道*/
  /* -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2); */
  border-radius: 0;
  background: rgba(85, 85, 85, 0.1);
}
</style>

希望路过的各位大佬提出改进意见
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值