vue2实现移动端级联选择器

级联选择器,用于多层级数据的选择,典型场景为省市区选择

1 简易版

1.1效果展示

vue2实现移动端级联选择器_第1张图片

1.2 组件源码

<template>
  <div>
    
    <div
      v-show="visible"
      class="mask"
      @click="close"
    />
    <transition name="fade">
      
      <div
        v-show="visible"
        class="cascader"
      >
        
        <div class="title">
          {{ title }}
          
          <i 
            v-if="leftIcon"
            :class="['back', {'arrow': leftIcon === 'arrow', 'cross': leftIcon === 'cross'}]"
            @click="close"
          />
        div>
        
        <ul class="nav-list line">
          <template v-for="(item, index) in (tabIndex + 1)">
            <li
              :key="index"
              :class="['nav-list-item', {'active': index === tabIndex, 'more-hidden': moreHidden}]"
              @click="changeIndex(index)"
            >
              {{ (selectedOptions[index] && selectedOptions[index][fieldNames.text]) || placeholderText }}
            li>
          template>
        ul>
        
        <ol 
          class="select-list"
          ref="selectListRef"
        >
          <template v-for="(item, index) in pickerDataArr[tabIndex]">
            <li
              :key="index"
              class="select-list-item line"
              @click="selectItem(item)"
            >
              {{ item[fieldNames.text] }}
            li>
          template>
        ol>
      div>
    transition>
  div>
template>

<script>
export default {
  name: 'cascaderPicker',
  props: {
    // 是否显示级联选择器,支持 .sync 修饰符
    visible: {
      type: Boolean,
      default: false
    },
    // 可选项数据源
    options: {
      type: Array,
      default: () => []
    },
    // 顶部标题
    title: {
      type: String,
      default: ''
    },
    // 各级未选中时的提示文案	 
    placeholderText: {
      type: String,
      default: '请选择'
    },
    // 各级选择的结果文字展示溢出是否隐藏
    moreHidden: {
      type: Boolean,
      default: false
    },
    // 自定义选择层级数
    level: {
      type: Number,
      default: 3
    },
    // 自定义options结构中的字段
    fieldNames: {
      type: Object,
      default: () => ({
        text: 'text',
        value: 'value',
        chlidren: 'children'
      })
    },
    // 关闭图标:arrow(左箭头)/cross(交叉)
    leftIcon: {
      type: String,
      default: 'arrow'
    }  
  },
  data() {
    return {
      // 当前选择级索引
      tabIndex: 0,
      // 各级选择的结果组合
      selectedOptions: [],
      // 每一级列表组合
      pickerDataArr: []
    }
  },
  watch: {
    tabIndex () {
      // 列表滚动置零
      this.$refs['selectListRef'].scrollTop = 0
    },
    // 每次弹起组件时重置数据
    visible (val) {
      if (val) {
        this.tabIndex = 0
        this.selectedOptions = []
        this.pickerDataArr = []
        // 默认展示第一级列表数据(最外层)
        this.pickerDataArr.push(this.options)
      }
    }
  },
  methods: {
    changeIndex (index) {
      // 往回从新选择,加visible条件防止关闭级联选择器动画误触
      if (this.visible && index < this.tabIndex) {
        // 删除选中当前级开始的结果
        this.selectedOptions.splice(index)
        // 删除选中当前级下一级的列表数据
        this.pickerDataArr.splice(index + 1)
        this.tabIndex = index
      }
    },
    selectItem (item) {
      // 关闭级联选择器动画防重
      if(!this.visible) return
      // 保存选择的结果
      this.selectedOptions.push(item)
      // 下一级没有子元素及子元素长度为0 或者 当前选择级数等于设定的层级数,结束选择
      if (!(item[this.fieldNames.children] && item[this.fieldNames.children].length) || this.tabIndex + 1 >= this.level) {
        this.$emit('finish', this.selectedOptions)
        return
      }
      // 保存下一级列表数据
      this.pickerDataArr.push(item[this.fieldNames.children])
      this.tabIndex++
    },
    close () {
      this.$emit('close')
      this.$emit('update:visible', false)
    }
  }
}
script>

<style lang="less" scoped>
.mask{
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);
  z-index: 99;
}
.cascader{
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 80%;
  background-color: #fff;
  border-radius: 16px 16px 0 0;
  display: flex;
  flex-direction: column;
  z-index: 999;
  .title{
    position: relative;
    padding: 20px 0;
    font-size: 18px;
    line-height: 18px;
    text-align: center;
    .back{
      position: absolute;
      top: 19px;
      left: 20px;
      width: 20px;
      height: 20px;
    }
    .arrow{
      background: url('./images/icon-arrow.png') no-repeat;
      background-size: 100%;
    }
    .cross{
      background: url('./images/icon-close.png') no-repeat;
      background-size: 100%;
    }
  }
  .nav-list{
    position: relative;
    display: flex;
    padding: 0 20px;
    font-size: 16px;
    text-align: center;
    .nav-list-item{
      padding: 14px 0;
      margin-right: 16px;
      max-width: 64px;
    }
    .nav-list-item:last-child{
      margin-right: 0;
    }
    .active{
      position: relative;
      color: #1752ff;
    }
    .active::after{
      position: absolute;
      left: 0;
      bottom: 0;
      display: block;
      content: '';
      width: 100%;
      height: 3px;
      background-color: #1752ff;
    }
    .more-hidden{
      align-items: center;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
  .select-list{
    flex: 1;
    overflow-y: auto;
    font-size: 16px;
    .select-list-item{
      position: relative;
      padding: 12px 20px;
    }
  }
  .line::after{
    position: absolute;
    left: 0;
    bottom: 0;
    display: block;
    content: '';
    width: 100%;
    height: 1px;
    background-color: #ccc;
    transform: scaleY(0.5);
  }
  .select-list::-webkit-scrollbar{
    display: none;
  }
}
.fade-enter,.fade-leave-to{
  opacity: 0;
  transform: translateY(100%);
}
.fade-enter-active,.fade-leave-active{
  transition: all 0.3s ease;
}
style>

2 高级版

2.1 效果展示

vue2实现移动端级联选择器_第2张图片

2.2 组件源码

<template>
  <div>
    
    <div
      v-show="visible"
      class="mask"
      @click="close"
    />
    <transition name="fade">
      
      <div
        v-show="visible"
        class="cascader"
      >
        
        <div class="title">
          {{ title }}
          
          <i 
            v-if="leftIcon"
            :class="['back', {'arrow': leftIcon === 'arrow', 'cross': leftIcon === 'cross'}]"
            @click="close"
          />
        div>
        
        <ul class="nav-list">
          <template v-for="(item, index) in navListLen">
            <li
              :key="index"
              :class="['nav-list-item', {'active': index === tabIndex, 'more-hidden': moreHidden}]"
              @click="changeIndex(index)"
            >
              {{ (selectedOptions[index] && selectedOptions[index][fieldNames.text]) || placeholderText }}
            li>
          template>
        ul>
        
        <div 
          class="select-content"
        >
          <div 
            class="select-content-box"
            :style="translateStyle"
          >
            <ol 
              class="select-list"
              v-for="(item1, index1) in pickerDataArr" :key="index1"
              ref="selectListRefs"
            >
              <template v-for="(item2, index2) in item1">
                <li
                  :key="index2"
                  class="select-list-item"
                  @click="selectItem(item2, index1)"
                >
                  <span class="text">{{ item2[fieldNames.text] }}span>
                  <i
                    v-if="selectedOptions.includes(item2)"
                    class="choose-active"
                  />
                li>
              template>
            ol>
          div>
        div>
      div>
    transition>
  div>
template>

<script>
export default {
  name: 'cascaderPicker',
  props: {
    // 是否显示级联选择器,支持 .sync 修饰符
    visible: {
      type: Boolean,
      default: false
    },
    // 可选项数据源
    options: {
      type: Array,
      default: () => []
    },
    // 顶部标题
    title: {
      type: String,
      default: ''
    },
    // 各级未选中时的提示文案	 
    placeholderText: {
      type: String,
      default: '请选择'
    },
    // 各级选择的结果文字展示溢出是否隐藏
    moreHidden: {
      type: Boolean,
      default: false
    },
    // 自定义选择层级数
    level: {
      type: Number,
      default: 3
    },
    // 自定义options结构中的字段
    fieldNames: {
      type: Object,
      default: () => ({
        text: 'text',
        value: 'value',
        chlidren: 'children'
      })
    },
    // 关闭图标:arrow(左箭头)/cross(交叉)
    leftIcon: {
      type: String,
      default: 'arrow'
    }
  },
  data() {
    return {
      // 当前选择级索引
      tabIndex: 0,
      // 各级选择的结果组合
      selectedOptions: [],
      // 每一级列表组合
      pickerDataArr: [],
      // 完成选择标识
      finishedFlag: false
    }
  },
  created () {
    // 默认展示第一级列表数据(最外层)
    this.pickerDataArr.push(this.options)
  },
  computed: {
    navListLen () {
      return !this.finishedFlag && this.selectedOptions.length < this.level ? this.selectedOptions.length + 1 : this.selectedOptions.length
    },
    translateStyle () {
      return {
        // 当选择级数改变时,需要切换当前列表位置
        transform:`translateX(-${this.tabIndex * 100}%)`,
        transitionDuration: '0.3s'
      }
    }
  },
  methods: {
    changeIndex (index) {
      // 关闭级联选择器时防止动画延迟误触
      if (!this.visible) return
      // 改变当前选择级数索引
      this.tabIndex = index
    },
    selectItem (item, clickIndex) {
      // 动画延迟防重:在关闭级联选择器前,点击列表选项所处的层级数与当前选择级数相等才能进行选择
      if (!this.visible || clickIndex !== this.tabIndex) return
      // 每次选择选项时,完成选择标识需重置,以防来回选择情况
      this.finishedFlag = false
      // 先删除当前级开始选中的旧数据,再保存当前级选中的结果
      this.selectedOptions.splice(this.tabIndex, this.selectedOptions.length, item)
      // 下一级没有子元素及子元素长度为0 或者 当前选择级数等于设定的层级数,结束选择
      if (!(item[this.fieldNames.children] && item[this.fieldNames.children].length) || this.tabIndex + 1 >= this.level) {
        this.finishedFlag = true
        this.$emit('finish', this.selectedOptions)
        return
      }
      // 下一级数据变更前,滚动位置需要置0
      this.$refs['selectListRefs'].forEach((ele, index) => {
        if (index > this.tabIndex) {
          ele.scrollTop = 0
        }
      })
      this.tabIndex++
      // 先删除下一级开始的旧选择列表数据,再保存当前的下一级列表数据
      this.pickerDataArr.splice(this.tabIndex, this.pickerDataArr.length, item[this.fieldNames.children])
    },
    close () {
      this.$emit('close')
      this.$emit('update:visible', false)
    },
    resetPicker () {
      this.tabIndex = 0
      this.finishedFlag = false
      this.selectedOptions = []
	  this.pickerDataArr = []
      this.pickerDataArr.push(this.options)
      this.$nextTick(() => {
        // 重置默认展示第一级列表的滚动位置
        if(this.$refs['selectListRefs'][0]) {
          this.$refs['selectListRefs'][0].scrollTop = 0
        }
      })
    }
  }
}
script>

<style lang="less" scoped>
.mask{
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, .7);
  z-index: 99;
}
.cascader{
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 80%;
  background-color: #fff;
  border-radius: 16px 16px 0 0;
  display: flex;
  flex-direction: column;
  z-index: 999;
  .title{
    position: relative;
    padding: 20px 0;
    font-size: 18px;
    line-height: 18px;
    text-align: center;
    .back{
      position: absolute;
      top: 19px;
      left: 20px;
      width: 20px;
      height: 20px;
    }
    .arrow{
      background: url('./images/icon-arrow.png') no-repeat;
      background-size: 100%;
    }
    .cross{
      background: url('./images/icon-close.png') no-repeat;
      background-size: 100%;
    }
  }
  .nav-list{
    display: flex;
    padding: 0 20px;
    font-size: 16px;
    text-align: center;
    .nav-list-item{
      padding: 14px 0;
      margin-right: 16px;
      max-width: 64px;
    }
    .nav-list-item:last-child{
      margin-right: 0;
    }
    .active{
      position: relative;
      color: #1752ff;
    }
    .active::after{
      position: absolute;
      left: 0;
      bottom: 0;
      display: block;
      content: '';
      width: 100%;
      height: 3px;
      background-color: #1752ff;
    }
    .more-hidden{
      align-items: center;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
  .select-content{
    flex: 1;
    overflow: hidden;
    .select-content-box{
      width: 100%;
      height: 100%;
      display: flex;
      .select-list{
        flex: 0 0 100%;
        width: 100%;
        height: 100%;
        overflow-y: auto;
        font-size: 16px;
        .select-list-item{
          padding: 12px 20px;
          display: flex;
          align-items: center;
          justify-content: space-between;
          .text{
            max-width: 264px;
          }
          .choose-active{
            width: 20px;
            height: 20px;
            background: url('./images/icon-choose.png') no-repeat;
            background-size: 100%;
          }
        }
      }
      .line{
        position: relative;
      }
      .line::after{
        position: absolute;
        left: 0;
        bottom: 0;
        display: block;
        content: '';
        width: 100%;
        height: 1px;
        background-color: #ccc;
        transform: scaleY(0.5);
      }
      .select-list::-webkit-scrollbar{
        display: none;
      }
    }
  }
}
.fade-enter,.fade-leave-to{
  opacity: 0;
  transform: translateY(100%);
}
.fade-enter-active,.fade-leave-active{
  transition: all 0.3s ease;
}
style>

3 使用说明

3.1 参数说明

参数 说明 类型 默认值
visible 是否显示级联选择器,支持 .sync 修饰符 Boolean false
options 可选数据源 Array []
title 顶部标题 String -
placeholder-text 各级未选中时的提示文案 String ‘请选择’
more-hidden 各级选择的结果文字展示溢出是否隐藏 Boolean false
level 自定义选择层级数 Number 3
field-names 自定义options结构中的字段 Object { text: ‘text’,value: ‘value’,chlidren: ‘children’}
left-icon 关闭图标:arrow(左箭头)/cross(交叉) String arrow

3.2 事件说明

事件 说明 回调参数
finish 各级选择完成后触发 selectedOptions(各级选择结果组合)
close 点击关闭图标或者遮罩时触发 ——

3.3 方法说明

方法 说明 参数
resetPicker 重置级联选择器(仅限高级版使用) ——

4 使用示例代码

<template>
  <div class="home">
    <div class="flex">
      <input type="text" v-model="cascaderValue">
      <button @click="showPicker">请选择所在地区button>
    div>
    <cascader-picker
      ref="cascaderPicker"
      :visible.sync="isShow"
      :options="options"
      title="请选择所在地区"
      :level="3"
      :field-names="{text: 'name', value: 'code', children: 'childList'}"
      @finish="onFinish"
    />
  div>
template>

<script>
import CascaderPicker from '../components/cascaderPicker'
import chinaArea from './json/chinaArea.json'
export default {
  name: 'Home',
  data() {
    return {
      cascaderValue: '',
      isShow: false,
      options: chinaArea?.childList || []
    }
  },
  components: {
    CascaderPicker
  },
  methods:{
    showPicker() {
      this.isShow = true
      //this.$refs.cascaderPicker.resetPicker() // 高级版有效
    },
    onFinish(selectedOptions) {
      console.log('选择结果==>', selectedOptions)
      this.cascaderValue = selectedOptions.map(item => item.name).join('/')
      this.isShow = false
    }
  }
}
script>
<style lang="less" scoped>
.flex{
  display: flex;
  align-items: center;
  padding: 20px;
  input{
    flex: 1;
  }
}
style>

5 省市区json数据

  • 点击链接获取数据
  • 数据举例:
{
    "code": 100000,
    "name": "中华人民共和国",
    "childList": [{
            "code": 110000,
            "name": "北京市",
            "childList": [{
                    "code": 110101,
                    "name": "东城区"
                },
                {
                    "code": 110102,
                    "name": "西城区"
                },
                {
                    "code": 110105,
                    "name": "朝阳区"
                },
                {
                    "code": 110106,
                    "name": "丰台区"
                },
                {
                    "code": 110107,
                    "name": "石景山区"
                },
                {
                    "code": 110108,
                    "name": "海淀区"
                },
                {
                    "code": 110109,
                    "name": "门头沟区"
                },
                {
                    "code": 110111,
                    "name": "房山区"
                },
                {
                    "code": 110112,
                    "name": "通州区"
                },
                {
                    "code": 110113,
                    "name": "顺义区"
                },
                {
                    "code": 110114,
                    "name": "昌平区"
                },
                {
                    "code": 110115,
                    "name": "大兴区"
                },
                {
                    "code": 110116,
                    "name": "怀柔区"
                },
                {
                    "code": 110117,
                    "name": "平谷区"
                },
                {
                    "code": 110118,
                    "name": "密云区"
                },
                {
                    "code": 110119,
                    "name": "延庆区"
                }
            ]
        }
    ]
}

你可能感兴趣的:(vue,移动端开发,vue.js)