基于vant UI设计一个下拉选择跟搜索功能的组件

2,385 阅读2分钟

简介:

这个是基于 vant3.0跟vue3.0的项目,在开发中封装的一个组件

效果图:

image.png image.png image.png

组件功能

1. 组件的下拉框数据进行排序从小到大,分别为四个字体及以下,一行占四个位置,四个字体以上十个字体以下,一行占2个位置均匀分布,超过十个字体,一行占一个位置,本来想过说用padding自动设置数据的宽度,但是效果不太好看,最后选择了这种方法

2.防抖跟节流,这个是在搜索框加的,进行模糊查询,输入字符,结束一秒后没有任何操作在进行请求

3.进行下拉框的遍历,下拉框为一个数组,根剧数据的数据遍历下拉框,以及下拉框的隐藏跟搜索框的隐藏

开发

第一步

首先先将下拉框改造成可以进行循环渲染的

参数:

title: 菜单项标题

menuList: 父组件传来的数据

<template>
     <div
        class="menu-list">
        <van-dropdown-menu>
          <van-dropdown-item
            v-for="(item,index) in menuList"
            :key="item.name"
            :title="self.activeName[index]||item.name"
            :ref="el => { nodes[index] = el }"
          >
              <div>自定义内容</div>
              <div class="dropdown-bottom">
              <van-button
                color="#F8F8F8"
                type="default"
                class="button"
                style="color:rgba(107,107,107,1)"
                @click="onfailed(index)"
              >
                重置
              </van-button>
              <van-button
                type="primary"
                class="button"
                @click="onConfirm(index,item.data)"
              >
                确定
              </van-button>
            </div>
          </van-dropdown-item>
        </van-dropdown-menu>
      </div>
  </template>
  <script>
import { onActivated, reactive, toRefs, ref } from 'vue'
export default {
props: {
    menuList: {
      type: Array,
      default: () => []
    },
  },
  setup (props, ctx) {
      const node = ref({})
      // 关闭的方法
      const onfailed = (index) => {
      // 置空title值
          self.activeName[index] = ''
          nodes.value[index].toggle()
          ctx.emit('onfailed', index)
    }
    // 确定方法
     const onConfirm = (index, item) => {
      if (self.menuValue[index] !== '') {
        const id = self.menuValue[index]
        // 确定以后修改title值
        self.activeName[index] = item[self.menuValue[index]].name
        // 数组的下标以及数据的下标
        const obj = {
          index,
          id
        }
        nodes.value[index].toggle()
        ctx.emit('menuChange', obj)
      }
    }
    return {
      ...toRefs(self),
      nodes,
      onfailed,
      onSelect,
      onConfirm,
      gridList
    }
  }
}
  </script>

第二步

添加搜索框以及搜索框的防抖节流方法

封装防抖节流

//common.js
// 节流函数
export function throttle (fn, wait) {
  // previous是上一次执行fn的时间,timer是定时器
  let previous = 0
  let timer = null
  // 将throttle处理结果当作函数返回
  return function (...args) {
    // 获取当前时间戳
    let now = +new Date()
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔
    if (now - previous < wait) {
      // 如果小于,则为本次触发操作设定一个新的定时器,定时器时间结束后执行函数fn
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        previous = now
        fn.apply(this, args)
      }, wait)
    } else {
      // 第一次执行或者时间间隔超出了设定的时间间隔,执行函数fn
      previous = now
      fn.apply(this, args)
    }
  }
}
// 防抖
export function debounce (fun, delay) {
  let t = ''
  return (args) => {
    let that = this
    let _args = args
    clearTimeout(t)
    t = setTimeout(function () {
      fun.call(that, _args)
    }, delay)
  }
}

添加搜索框

参数:

search: 判断搜索框是否显示

searchValue: 搜索框的值

formatter: 搜索的值去除前后空格的方法

placeholder: 占位符的值

onSearch: 确定搜索时触发的事件

updateSearch: 字符变动时触发的事件

<template>
<van-search
  v-if="search"
  v-model="searchValue"
  autocomplete="off"
  class="search"
  :formatter="formatter"
  :placeholder="placeholder"
  @search="onSearch"
  @update:model-value="updateSearch"
/>
</template>
<script>
import { onActivated, reactive, toRefs, ref } from 'vue'
// 引入刚才写的节流跟防抖函数
import { throttle, debounce } from '@/utils/common.js'
export default {
    props: {
        placeholder: {
          type: String,
          default: ''
        }
        search: {
          type: Boolean,
          default: true
        }
     }
     const self = reactive({
      searchValue: '',
     })
       const formatter = (val) => {
      return val.trim()
    }
    /**
     * @description: 点击搜索或者确定的键盘触发
     * @param {*} val 当前的值
     * @return {*}
     */
    const onSearch = throttle((val) => {
      ctx.emit('updateSearch', val)
    }, 1000)
    /**
     * @description: 搜索的触发
     * @param {*} item
     * @return {*}
     */
    const updateSearch = debounce((item) => {
      ctx.emit('updateSearch', item)
    }, 1000)
    return {
      ...toRefs(self),
      updateSearch,
      onSearch,
      formatter
    }
}
</script>

结尾

这个搜索框以及下拉其实就是,在选中以后将当前选中的对象的下标以及对象里边渲染的下标传给父组件,父组件进行筛选获得当前选中的值,进行下一步操作,简洁了每次需要下拉的时候写一大堆重复代码,并且搜索的时候模增加了防抖与节流 附上组件代码

<template>
  <div class="container-view">
    <div class="search-menu">
      <div
        v-if="menu"
        class="menu-list"
      >
        <van-dropdown-menu>
          <van-dropdown-item
            v-for="(item,index) in menuList"
            :key="item.name"
            :ref="el => { nodes[index] = el }"
            :disabled="disabled"
            :title-class="activeName[index] !== ''?'title-active':''"
            :title="activeName[index] || item.name"
          >
            <div class="status-list">
              <div
                v-for="(v,i) in item.data"
                :key="i"
                :class="menuValue[index] === i?`${gridList(v.name)} list status`:`${gridList(v.name)} list`"
                @click="onSelect(index,i)"
              >
                <span>
                  {{ v.name }}
                </span>
              </div>
            </div>
            <div class="dropdown-bottom">
              <van-button
                color="#F8F8F8"
                type="default"
                class="button"
                style="color:rgba(107,107,107,1)"
                @click="onfailed(index)"
              >
                重置
              </van-button>
              <van-button
                type="primary"
                class="button"
                @click="onConfirm(index,item.data)"
              >
                确定
              </van-button>
            </div>
          </van-dropdown-item>
        </van-dropdown-menu>
      </div>

      <van-search
        v-if="search"
        v-model="searchValue"
        autocomplete="off"
        class="search"
        :formatter="formatter"
        :placeholder="placeholder"
        @search="onSearch"
        @update:model-value="updateSearch"
      />
    </div>
  </div>
</template>

<script>
import { onActivated, onDeactivated, reactive, toRefs, ref, watch } from 'vue'
import { throttle, debounce } from '@/utils/common.js'
export default {
  name: 'MenuSearch',
  props: {
    placeholder: {
      type: String,
      default: ''
    },
    disabled: {
      type: Boolean,
      default: false
    },
    menu: {
      type: Boolean,
      default: true
    },
    search: {
      type: Boolean,
      default: true
    },
    // 这里使用的时候将默认值去掉,这里只是说明需要什么样子的数据
    menuList: {
      type: Array,
      default: () =>  [
  {
    name: '楼栋类型',
    data: [
      {
        value: '居住小区',
        name: '居住小区'
      },
      {
        value: '城中村',
        name: '城中村'
      },
      {
        value: '其他',
        name: '其他'
      }
    ]
  },
  {
    name: '使用用途',
    data: [
      {
        value: 1,
        name: '综合'
      },
      {
        value: 2,
        name: '住宅'
      },
      {
        value: 3,
        name: '商住'
      },
      {
        value: 4,
        name: '商业'
      },
      {
        value: 5,
        name: '厂房'
      },
      {
        value: 6,
        name: '仓库'
      },
      {
        value: 7,
        name: '办公'
      },
      {
        value: 8,
        name: '其他'
      },
      {
        value: 9,
        name: '公共设施'
      }
    ]
  }
]
    }
  },
  setup (props, ctx) {
    const nodes = ref({})
    const self = reactive({
      searchValue: '',
      timeer: null,
      flg: false,
      menuValue: [],
      activeMenu: [],
      activeName: ['', ''],
      list: props.menuList
    })
    const formatter = (val) => {
      return val.trim()
    }
    /**
     * @description: 点击搜索或者确定的键盘触发
     * @param {*} val 当前的值
     * @return {*}
     */
    const onSearch = throttle((val) => {
      ctx.emit('updateSearch', val)
    }, 1000)
    /**
     * @description: 搜索的触发
     * @param {*} item
     * @return {*}
     */
    const updateSearch = debounce((item) => {
      ctx.emit('updateSearch', item)
    }, 1000)
    /**
     * @description: onfailed 清空
     * @param {*} index 当前数组的下标
     * @return {*}
     */
    const onfailed = (index) => {
      self.activeName[index] = ''
      nodes.value[index].toggle()
      self.menuValue[index] = ''
      self.activeMenu[index] = ''
      ctx.emit('onfailed', index)
    }
    // 遍历获取元素,然后放在一个数组里
    /**
     * @description: onSelect 选择
     * @param {*} index 当前数组的下标
     * @param {*} i 当前选择的数
     * @return {*}
     */
    const onSelect = (index, i) => {
      self.menuValue[index] = i
      self.activeMenu[index] = i
    }
    /**
     * @description: onConfirm 确定方法
     * @param {*} index 当前数组的下标
     * @param {*} item 当前选择的数的数组
     * @return {*}
     */
    const onConfirm = (index, item) => {
      if (self.menuValue[index] !== '') {
        const id = self.menuValue[index]
        self.activeName[index] = item[self.menuValue[index]].name
        const obj = {
          index,
          id
        }
        nodes.value[index].toggle()
        ctx.emit('menuChange', obj)
      }
    }
    /**
     * @description: 动态修改宽
     * @param {*} value 当前显示的字符串
     * @return {*}
     */
    const gridList = (value) => {
      const length = value.replace(/[^\u4e00-\u9fa5]/gi, '').length
      return length <= 4 ? 'text-four' : length <= 10 ? 'text-two' : 'text-one'
    }
    onActivated(() => {
      self.searchValue = localStorage.getItem('searchValue') || ''
      self.activeName = localStorage.getItem('activeName') ? JSON.parse(localStorage.getItem('activeName')) : ['', '']
      props.menuList.forEach(v => {
        const obj = v
        obj.data.sort((a, b) => {
          return a.name.length - b.name.length
        })
        // return obj
      })
      self.menuValue = localStorage.getItem('activeName') ? self.activeMenu : []
    })
    onDeactivated(() => {
      localStorage.setItem('searchValue', self.searchValue)
      localStorage.setItem('activeName', JSON.stringify(self.activeName))
      self.menuValue = []
    })
    watch(() => props.menuList, (newValue) => {
      if (newValue.length > 0 && newValue) {
        props.menuList.forEach(v => {
          const obj = v
          obj.data.sort((a, b) => {
            return a.name.length - b.name.length
          })
          // return obj
        })
      }
    })
    return {
      ...toRefs(self),
      nodes,
      updateSearch,
      onSearch,
      formatter,
      onfailed,
      onSelect,
      onConfirm,
      gridList
    }
  }
}

</script>
<style scoped lang='less'>
.container-view {
  width: 100%;
  .search-menu {
    width: 100%;
    height: 48px;
    background: #ffffff;
    border-radius: 16px 16px 0px 0px;
    display: flex;
    align-items: center;
    // justify-content: space-around;
    padding: 0 12px;
    .menu-list {
      display: flex;
      height: 100%;
      .status-list {
        display: flex;
        background: #f8f8f8;
        align-content: flex-start;
        flex-wrap: wrap;
        overflow: auto;
        row-gap: 10px;
        min-height: 0px;
        max-height: 178px;
        padding-bottom: 10px;
        &::-webkit-scrollbar {
          display: none;
        }

        .list {
          // padding: 0px 0 16px 14px;
          display: flex;
          align-items: center;
          justify-content: center;
          background: #ffffff;
          border-radius: 2px;
          font-size: 14px;
          color: #666666;
          letter-spacing: 0;
          font-weight: 400;
          display: flex;
          align-items: center;
          white-space: nowrap;
          height: 29px;
          margin-top: 10px;
          &.status {
            // span {
            color: #ffffff;
            background: #3388ff;
            border-radius: 2px;
            // }
          }
          &.text- {
            &one {
              width: 90%;
              margin-left: 5%;
              margin-right: 5%;
            }
            &two {
              width: calc(100% / 2 - 20px);
              margin: 10px 10px 0 10px;
            }
            &four {
              width: calc(100% / 4 - 20px);
              margin: 10px 10px 0 10px;
            }
          }
        }
      }
      .dropdown-bottom {
        padding: 10px 12px;
        display: flex;
        justify-content: space-between;
        .button {
          width: 169px;
          height: 46px;
          font-size: 16px;
          letter-spacing: 0;
          font-weight: 400;
        }
      }
      .van-dropdown-item {
        top: 168px !important;
        position: fixed;
      }

      :deep(.van-dropdown-menu) {
        display: flex;
        align-items: center;

        .van-dropdown-menu__bar {
          .van-dropdown-menu__title {
            &.title-active {
              color: #327ee7;
            }
          }
        }

        .van-dropdown-menu__bar {
          height: 28px;

          .van-dropdown-menu__item {
            width: 90px;
            margin-right: 6px;
            background: #f2f2f2 !important;
             border-radius: 4px;
          }
          .van-dropdown-menu__title {
            &--active {
              color: #3388ff;
              &:after {
                border-color: transparent transparent #3388ff #3388ff !important;
              }
            }
            .van-ellipsis {
              width: 60px;
            }
            &:after {
              right: 0px;
            }
            padding: 0 10px;
            font-size: 14px;
            color: #666666;
            letter-spacing: 0;
            font-weight: 400;
          }
        }
      }
    }

    :deep(.search) {
      background: #f2f2f2;
      border-radius: 4px;
      flex: 1;
      height: 28px;
      padding: 0 10px;
      .van-search__content {
        width: 100%;
        padding: 0;
      }
      .van-cell {
        height: 100%;
        padding: 0;
      }
    }
  }
}
</style>