Vue搜索下拉与层级多选组件

1,836 阅读2分钟

在平时的前端开发过程中,我们习惯了选择已有的UI组件进行组合,比如常用的Element UI,大部分的组件都能满足日常的工作需要,比如下拉搜索,比如多层级搜索,有时候由于特定业务高度定制化场景,需要在同时既有多层级选择功能,又具备关键字搜索功能,这时候候就需要我们自定义组件了

默认打开搜索框时显示出一级层级,左侧的全球以洲类列表 图一

点击各洲再查询出各洲国家,并且可以多选,层级实际根据业务需求,可以是二级,三级及多级 图二

当输入关键字时显示下查出国家列表 关键字删除时显示为默认一级层级列表 鼠标点击别处关闭下拉框或者层级框

由于业务高度定制化,并且除以上正常交互外还有其它隐藏交互这时候必须从业务出发开始设计一个满足业务场景的多层级多选搜索组件,目前项目已上线,可正常使用,小伙伴们有相同场景需求时可以参考实现思路或者直接拿去修改使用。最开始有考虑过用一个下拉组件与一个层级组件进行互相显示和隐藏的功能,在实际的实现过程中产生不同程度问题不太好的体验以及bug,前且做不到按需定制功能,用别人的组件方便是方便,但是如果在上面再添加一些没有的功能,花费的成本就会比较大,既然是定制化场景,那就只能自定义了。

目前实现的方式是共用一个input框进行搜索关键字的操作, 根据关键字的长度显示层级多选组件或者下拉多选组件,项目使用的Element UI,所以最外层直接使用了¶Popover 弹出框, 搜索框 el-input, 二级面板及搜索面板分别是Ul

Template

<template>
  <div class="search">
    <el-popover
      v-model="visible"
      :disabled="disabled"
      popper-class="searchBox"
      placement="bottom"
      width="850"
      trigger="click"
      @hide="canclePop"
    >
      <!-- 搜索框 -->
      <el-input
        slot="reference"
        v-model="value"
        placeholder="请选择或搜索需要的地区"
        :disabled="disabled"
        class="input"
        @input="keyworcChange"
      />
      <!-- 二级面板 -->
      <div v-show="value.length == 0" class="box">
        <!-- 父级 -->
        <ul v-loading="loading" class="parent">
          <li
            v-for="(item,index) in areaList"
            :key="'CONTINENT' +item.id"
            :class="{'active': activeIndex == index}"
            @click="getAreaCityList(item,index)"
          >
            <p :class="{'global': item.isGloBal>{{ item.name }}</p>
            <i v-if="item.placeType" class="el-icon-arrow-right" />
          </li>
        </ul>
        <!-- 子级 -->
        <ul v-loading="sonLoading" class="son">
          <li v-show="areaCityList.length == 0" class="noData">暂无数据</li>
          <li v-for="(item,index) in areaCityList" :key="'city' +index +item.id">
            <p>
              <el-checkbox v-model="item.checked" :disabled="item.disabled" @change="val=>chooseContry(val,item)">{{ item.name }}</el-checkbox>
            </p>
          </li>
        </ul>
      </div>
      <!-- 搜索面板 -->
      <div v-show="value.length > 0" class="select">
        <ul v-loading="sonLoading">
          <li v-show="searchList.length == 0" class="noData">暂无数据</li>
          <li v-for="(item,index) in searchList" :key="'searchCity' + index + item.id">
            <p>
              <el-checkbox v-model="item.checked" @change="val=>chooseSearchList(val,index)">{{ item.name }}</el-checkbox>
            </p>
          </li>
        </ul>
      </div>
      <!-- 按钮组 -->
      <div class="btn-list">
        <el-button type="primary" size="small" @click="setCity">确认</el-button>
        <el-button size="small" @click="canclePop">取消</el-button>
      </div>
    </el-popover>
  </div>
</template>

JavaScript

<script>
export default {
  props: {
    // 组件是否禁用
    disabled: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      loading: false,	
      sonLoading: false,
      selectLoading: false,
      // 搜索关键字
      value: '',
      // 层级列表是否显示
      visible: false,
      // 当前选中的父级索引
      activeIndex: -1,
      // 区块数据列表
      areaList: [],
      // 区块子级城市列表
      areaCityList: [],
      // 搜索城市列表
      searchList: [],
      // 区块城高选中列表
      cityChecked: [],
      // 搜索城市选中列表
      searchCityChecked: []
    }
  },
  created() {
    this.getAreaList()
  },
  methods: {
    // 获取初始化区块列表
    getAreaList() {
      this.loading = true
      this.areaList = [
        { id: '', name: '全球', placeType: '' },
        { id: 1, name: '亚洲', placeType: 'CONTINENT' },
        { id: 2, name: '东南亚', placeType: 'CONTINENT' },
        { id: 3, name: '非亚', placeType: 'CONTINENT' }
      ]
      this.areaCityList = []
      this.loading = false
    },
    // 获取子级数据
    getAreaCityList(item, index) {
      // 选择全球
      if (!item.id) {
        this.cityChecked = []
        item.idArr = this.areaList.filter(item => item.id)
        item.isGloBal = true
        this.$emit('setCity', [item])
        this.visible = false
        return
      }
      // 选择其它地区
      this.sonLoading = true
      this.activeIndex = index
      setTimeout(() => {
        this.sonLoading = false
        this.areaCityList = [
          { id: '', name: '全部', placeType: '', checked: false, disabled: false },
          { id: 7, name: '中国', placeType: 'COUNTRY', checked: false, disabled: false },
          { id: 8, name: '美国', placeType: 'COUNTRY', checked: false, disabled: false },
          { id: 9, name: '韩国', placeType: 'COUNTRY', checked: false, disabled: false }
        ]
        // 设置已有选中效果
         const index = this.areaCityList.findIndex(cur => cur.id === checkedItem.id)
        if (index !== -1) {
          if (checkedItem.placeType === 'CONTINENT') {
            this.areaCityList.forEach(cur => {
              cur.disabled = true
              cur.checked = true
            })
            this.areaCityList[index].disabled = false
          } else {
            this.areaCityList.forEach(cur => {
              if (cur.placeType === 'CONTINENT') {
                cur.disabled = true
              }
            })
          }
          this.areaCityList[index].checked = true
      }, 500)
    },
    // 搜索对应的国家列表
    keyworcChange(val) {
      if (val.length === 0) {
        this.searchCityChecked = []
      }
      !this.visible && (this.visible = true)
      this.cityChecked = []
      this.searchCityChecked = []
      this.selectLoading = true
      setTimeout(() => {
        this.selectLoading = false
        this.searchList = [
          { id: 10, name: '中国6', placeType: 'COUNTRY', checked: false },
          { id: 11, name: '美国7', placeType: 'COUNTRY', checked: false },
          { id: 12, name: '韩国8', placeType: 'COUNTRY', checked: false }
        ]
      }, 500)
    },
    // 二级国家数据选择
    chooseContry(val, item) {
      // ALL checked
      if (!item.placeType) {
        if (val) {
          item.type = 'All'
          item.id = this.areaList[this.activeIndex].id
          item.alias = this.areaList[this.activeIndex].name
          this.areaCityList.forEach(city => {
            if (city.placeType) {
              city.disabled = true
              city.checked = true
            }
          })
        } else {
          this.areaCityList.forEach(city => {
            if (city.placeType) {
              city.disabled = false
              city.checked = false
            }
          })
        }
      } else { // other checked
        const hasChecked = this.areaCityList.some(item => item.checked)
        const index = this.areaCityList.findIndex(item => !item.placeType)
        if (hasChecked && index !== -1) {
          this.areaCityList[index].disabled = true
        } else {
          this.areaCityList[index].disabled = false
        }
      }

      // 临时存储子级选中的数据
      if (val) {
        this.cityChecked.push(item)
      } else {
        const itemIndex = this.cityChecked.findIndex(city => city.id === item.id)
        this.cityChecked.splice(itemIndex, 1)
      }
    },
    // 选中搜索列表
    chooseSearchList(val, index) {
      const item = this.searchList[index]
      if (val) {
        this.searchCityChecked.push(item)
      } else {
        const checkedIndex = this.searchCityChecked.findIndex(cur => item.id === cur.id)
        this.searchCityChecked.splice(checkedIndex, 1)
      }
    },
    // 搜索框隐藏 清空选中的面板数据和搜索列表
    canclePop() {
      this.visible = false
      this.activeIndex = -1
      this.areaCityList = []
      this.searchList = []
      this.cityChecked = []
      this.searchCityChecked = []
    }, 
     // 设置已选城市
    setCity() {
      if (this.value.length > 0) {
        this.$emit('setCity', this.searchCityChecked)
        this.searchCityChecked = []
      } else {
        this.$emit('setCity', this.cityChecked)
        this.cityChecked = []
      }
      this.value = ''
      this.visible = false
    }
  }
}
</script>

SCSS

<style lang="scss" scoped>
.searchBox {
  .box {
    display: flex; height: 160px; overflow: hidden;
    li {
       padding: 0 8px; display: flex;  justify-content: space-between; align-items: center;
       &:hover {
        color: #3678DC;
       }
       p {
          overflow: hidden; text-overflow: ellipsis; height: 34px; line-height: 34px; white-space: nowrap; word-break: break-all;
       }
    }
    .parent {
      width: 300px; height: 100%; overflow: scroll; padding:6px 0;
      li {
        cursor: pointer;
      }
      .active {
        color: #3678DC;
      }
      .global {
        color: #999;
      }
     }
     .son {
       width: 650px; height: 100%; overflow: scroll; padding:6px 0; border-left:solid 1px #E4E7ED; padding-left:20px;
    }
   }
  .select {
    height: 160px; overflow: auto; padding-left:20px;
    li {
      line-height: 2em; 
      &:hover {
        color: #3678DC;
      }
    }
  }
  .btn-list {
    text-align: right;
  }
  li.noData {
    padding-top:66px; justify-content: center;  color: #3678DC; text-align: center;    
  }  
  
}
</style>

结语

目前该组件只是一个特殊场景下的定制化组件,满足了当前业务的功能,并且可能随着需求的变更可以快速修改使用。以上代码删减了一些业务相关的字段传值,提供一个实现的想法思路。可以根据该方案进行不同程度的扩展和使用。