二次封装Ant-design-vue下拉框组件,支持分页操作、滚动加载、模糊搜索等

哈喽,各位高贵的倔友们你们好呀,今天摸鱼摸得可还开心(^▽^)?记住加班不是福报,摸鱼才是王道,打工人加油(✪ω✪)。

那么,如果各位亲们摸累了,不如来看看文章学点小知识解解乏吧,今天小编给各位带了一篇实战类文章,一篇关于ant-design-vue下拉框组件分页二次封装小技巧的文章。各位如果觉得小编写得还不错的话,希望能给点个赞呗,话就不多说啦,直接进入正题。

写在开头

以下代码小编使用 Vue 技术栈来演示,并且 ant-design-vue (以下简称AntV)使用的是 1.7.8 版本,小编一直以来觉得这个版本真的非常好用,如果你项目中使用的是 Vue2 那我强烈推荐你使用它(★ᴗ★)。

安装并引入AntV

安装:

javascript 复制代码
npm install ant-design-vue@1.7.8 --save

main.js 文件中引入:

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'

import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
Vue.use(Antd)

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

这里我们使用全局引入的方式,更多其他引入方式可以自行去官网查看。传送门

数据准备

小编先准备了一些测试数据,下面会以这种数据结构来编写逻辑,如果你真实的数据结构与此不同,可以自行再转换一下或者适当调整一下代码逻辑。

创建 data.js 文件:

javascript 复制代码
const data = [
  { label: '雷进宝', value: 1 },
  { label: '林雅南', value: 2 },
  { label: '小黑', value: 3 },
  { label: '洪振霞', value: 4 },
  { label: '蔡莉华', value: 5 },
  { label: '小蓝', value: 6 },
  { label: '李军', value: 7 },
  { label: '小橙', value: 8 },
  { label: '王军', value: 9 },
  { label: '小黄', value: 10 },
  { label: '小棕', value: 11 },
  { label: '瀚文', value: 12 },
  { label: '赵浩歌', value: 13 },
  { label: '陈浩', value: 14 },
  { label: '安鹏飞', value: 15 },
  { label: '刘茂强', value: 16 },
  { label: '邓海阳', value: 17 },
  { label: '陈婉璇', value: 18 },
  { label: '小粉粉', value: 19 },
  { label: '方一强', value: 20 },
  { label: '茹定', value: 21 },
  { label: '王汝民', value: 22 },
  { label: '郑昌梦', value: 23 },
  { label: '须承文', value: 24 },
];

export function getDataAPI({ pageNum, pageSize, keyword }) {
  return new Promise(resolve => {
    setTimeout(() => {
      const listData = [];
      data.forEach(item => {
        if(keyword) {
          if(item.label.indexOf(keyword) !== -1) {
            listData.push(item);
          }
        } else {
          listData.push(item);
        }
      })
      const start = (pageNum - 1) * pageSize;
      const end = pageNum * pageSize;
      resolve({
        rows: listData.slice(start, end),
        total: listData.length,
      });
    }, 500)
  })
}

getDataAPI 方法用来模拟数据接口分页的情况,它通过 pageNumpageSize 参数来控制分页,也支持 keyword 参数来进行模糊搜索。

基础分页功能

我们先来创建一个 SelectPaging.js 文件,具体内容如下:

javascript 复制代码
<template>
  <a-select
    class="select"
    v-model="selectedValue"
    v-bind="getProps"
    :loading="loading"
    :allowClear="!loading"
    @dropdownVisibleChange="dropdownVisibleChange"
  >
    <a-icon @click="clear" slot="clearIcon" type="close-circle" theme="filled" />
    <div slot="dropdownRender" slot-scope="menu">
      <v-nodes :vnodes="menu" />
      <a-divider class="divider" />
      <div class="paging-box" @mousedown="e => e.preventDefault()">
        <a-pagination
          size="small"
          :current="params.pageNum"
          :pageSize="params.pageSize"
          :total="total"
          @change="paginationChange"
        />
      </div>
    </div>
    <a-select-option
      v-for="item in listData"
      :key="item.value"
      :value="item.label"
    >
      <!-- :value="item.label" 绑定 label 属性 -->
      {{ item.label }}
    </a-select-option>
  </a-select>
</template>

<script>
import { getDataAPI } from './data';

/** @name 定义一些下拉框组件的默认值,为了更好的满足业务逻辑 **/
const DEFAULT_PROPS = {
  mode: 'default', // 仅支持这两种模式: default/multiple
  placeholder: '请选择',
  defaultActiveFirstOption: false, // 是否默认高亮第一个选项, 默认是true
  showArrow: false, // 不使用默认的清除按钮,使用slot插入清除按钮,更好的控制清除事件
  optionLabelProp: 'children' // 回填到选择框的 option 的属性值,默认是 Option 的子元素
  showSearch: false, // 是否支持输入搜索
  filterOption: false, // 不使用默认的筛选规则,因为分页行为,筛选需要传递到接口层面去过滤
}

export default {
  components: {
    VNodes: {
      functional: true,
      render: (_, ctx) => ctx.props.vnodes
    }
  },
  props: {
    value: {
      type: [Number, String, Array],
      default: undefined
    },
  },
  data() {
    return {
      loading: false,
      params: {
        pageNum: 1,
        pageSize: 5,
        keyword: ''
      },
      listData: [],
      total: 0,
      selectedValue: undefined,
      open: false, // 用于处理 search 事件默认最终会输出一次空的行为, 其主要会造成与用户删除的行为冲突
    }
  },
  computed: {
    getProps () {
      return {
        ...DEFAULT_PROPS,
        ...this.$attrs
      }
    }
  },
  methods: {
    /** @name 分页操作 **/
    paginationChange(page) {
      this.params.pageNum = page;
      this.getData();
    },
    /** @name 点击清除按钮 **/
    clear () {
      this.params = {
        pageNum: 1,
        pageSize: 5,
        keyword: '',
      }
      this.selectedValue = undefined;
      this.getData();
    },
    /** @name 展开下拉菜单的回调 **/
    dropdownVisibleChange(open) {
      this.open = open;
      if (open) {
        if (this.loading) return;
        if (this.listData.length !== 0 && this.params.pageNum === 1 && this.params.keyword === '') return;
        // 重置回第一页
        this.resetData();
      } else {
        this.params.keyword = '';
      }
    },
    /** @name 重置下拉菜单数据 **/
    resetData() {
      this.params.pageNum = 1;
      this.params.keyword = '';
      this.getData();
    },
    /** @name 获取数据 **/
    getData () {
      this.loading = true
      getDataAPI(this.params).then(res => {
        this.listData = res.rows
        this.total = res.total
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
  }
}
</script>

<style scoped>
.select {
  width: 100%;
}
.divider {
  margin: 2px 0;
}
.paging-box {
  display: flex;
  justify-content: flex-end;
  box-sizing: border-box;
  padding: 4px 8px 8px 0;
}
</style>

在上面代码中,小编写了一些注释,相信你看起来应该不难,我们主要是使用到了 Select 组件中的 dropdownRender 属性来完成自定义下拉框内容。

对于为什么创建 <v-nodes /> 组件,你可以简单认为就是创建了一个函数式组件,用于渲染下拉框的选项内容(menu),这样渲染能方便我们将自定义内容与下拉框选项内容结合,我们可以令自定义的内容在其上面渲染,或者在其下方渲染。

具体使用及其效果:

javascript 复制代码
<template>
  <select-paging v-model="form.baseValue" />
</template>

<script>
import SelectPaging from './components/SelectPaging.vue'

export default {
  components: {
    SelectPaging
  },
  data() {
    return {
      form: {
        baseValue: undefined,
      }
    }
  },
}
</script>

多选功能

对于多选功能,我们还是一样继承 AntV 原本的用法,直接添加 mode="multiple" 属性即可。

javascript 复制代码
<template>
  <select-paging v-model="form.multipleValue" mode="multiple" />
</template>

<script>
export default {
  ...
  data() {
    return {
      form: {
        multipleValue: [],
      }
    }
  },
}
</script>

处理v-model

不过,到这里代码还没写完,我们还需要把 v-model 绑定的行为处理一下,让数据实现双向数据绑定。

xml 复制代码
<template>
  <a-select
    ...
    @change="change"
  >
    ...
    <a-select-option
      ...
      :option="item"
    >
      <!-- :option="item" 绑定整个数据项,方便后续的使用 -->
      {{ item.label }}
    </a-select-option>
  </a-select>
</template>

<script>
import SelectPaging from './components/SelectPaging.vue'

export default {
  ...,
  watch: {
    value: {
      handler: function() {
        this.selectedValue = this.value
      },
      deep: true,
      immediate: true
    },
    selectedValue () {
      this.$emit('input', this.selectedValue)
    },
  },
  methods: {
    ...,
    /** @name 下拉框选择 **/
    change(selectedValue, options) {
      let selectedOption;
      if (Array.isArray(options)) { // 处理多选
        selectedOption = options.map(item => item.data.attrs.option);
      } else {
        // 这里取的option就是上面标签 :option="item" 绑定的数据项
        selectedOption = options.data.attrs.option;
      }
      this.$emit('change', selectedValue, selectedOption)
    },
  }
}
</script>

到这里不知道你发现没有,通过 v-model 绑定的值,拿到的会是每条数据中的 label 属性,但是,很多时候后端接口需要我们提交的都是数据中的 value 属性。

<a-select-option ... :value="item.label" />

那么,这里就要强调一下了,由于下拉框组件是分页性质的,所以这意味着我们需要把 label 属性储存起来,提供给回显的时候使用。

这里有的人可能会在想,我在添加的时候,提交 value 属性,而在回显的时候,后端接口能根据 value 顺便把 label 给我查回来,这样是否可行?如果你仅仅使用该基础分页功能,是可行的,但如果你还使用后面的 "支持自定义输入的值" 功能,那这就不行了,又会引出其他一系列问题。

所以总得来说,最好是 labelvalue 一起存储,或者只需要 label 就只存 label 就行。

而对于还想获取选项中的其他属性,可以通过 change 事件来解决。

javascript 复制代码
<template>
  <select-paging v-model="form.baseValue" @change="change" />
</template>

<script>
...
export default {
  ...,
  methods: {
    change(selectedValue, options) {
      console.log(selectedValue, options)
    },
  }
}
</script>

模糊搜索功能

既然需要分页行为,这意味着下拉列表数据应该是很多的,那么我们就不可能一页一页翻着去找,搜索得给它安排上才行(✪ω✪)。

javascript 复制代码
<template>
  <a-select
    ...
    @search="search"
  >
    ...
  </a-select>
</template>

<script>
...
export default {
  ...,
  methods: {
    /** @name 搜索事件 **/
    search(value) {
      if(!this.getProps.showSearch) return;
      if(this.open) {
        const inputValue = value || undefined;
        this.params.keyword = inputValue;
        this.params.pageNum = 1;
        this.getData();
      }
    },
  }
}
</script>

具体使用:

html 复制代码
<template>
  <div>
    <select-paging v-model="form.searchValue" :showSearch="true" />
    <select-paging v-model="form.searchValueMultiple" :showSearch="true" mode="multiple" />
  </div>
</template>

是否支持模糊搜索功能,我们可以直接通过 showSearch 属性来开启。

防抖优化

对于这种一旦输入就请求接口的行为,一般我们需要做一些优化策略才行,防止性能损耗,提高用户体验。

javascript 复制代码
<script>
...
import _ from 'lodash';

export default {
  ...,
  methods: {
    ...,
    /** @name 搜索事件 **/
    search: _.debounce(async function (value) {
      if(!this.getProps.showSearch) return;
      if(this.open) {
        const inputValue = value || undefined;
        this.params.keyword = inputValue;
        this.params.pageNum = 1;
        this.getData();
      }
    },
  }
}
</script>

防抖函数就不多说啦,你可以下载 lodash 或者可以自己手撸一个。

javascript 复制代码
npm install lodash --save

支持自定义输入的值

下拉框组件很多时候的使用场景就是用来选择下拉选项的,但是有时业务需求可能要求它既要能选择也要能输入,那么我们应该如何来完成这么一个功能,接下来就随小编一起来瞧瞧吧。

javascript 复制代码
<template>
  <a-select
    ...
    @inputKeydown="inputKeydown"
  >
    ...
  </a-select>
</template>

<script>
...

export default {
  ...,
  props: {
    ...,
    /** @name 是否支持自定义输入的值,单选-直接输入即可,多选-需要回车操作 **/
    isCustomInput: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    ...,
    /** @name 展开下拉菜单的回调 **/
    dropdownVisibleChange(open) {
      this.open = open;
      if (open) {
        if (this.loading) return;
        if (this.listData.length !== 0 && this.params.pageNum === 1 && this.params.keyword === '') return;
        this.resetData();
      } else {
        // 不支持的时候,关闭就清空
        if (!this.isCustomInput) {
          this.params.keyword = '';
        }
      }
    },
    /** @name 搜索事件 **/
    search: _.debounce(async function (value) {
      if(!this.getProps.showSearch) return;
      if(this.open) {
        const inputValue = value || undefined;
        if (this.isCustomInput) {
          // 单选-存储输入的值
          if (this.getProps.mode !== 'multiple') {
            this.selectedValue = inputValue;
          }
        }
        this.params.keyword = inputValue;
        this.params.pageNum = 1;
        this.getData();
      }
    },
    /** @name 键盘按下时回调 **/
    inputKeydown ({ code, target }) {
      // 按下回车键
      if (code === 'Enter' && this.isCustomInput) {
        const inputValue = target.value;
        this.selectedValue.push(inputValue);
        this.params.keyword = '';
      }
    },
  }
}
</script>

功能实现起来也不难,在单选的时候,只要将用户输入的内容直接赋值到 selectedValue 变量身上就行了,多选的时候就需要我们监听 inputKeydown 事件手动处理用户输入的内容。

如果有对 AntV 比较熟的小伙伴可能知道, a-select 组件的 mode 属性还支持 tags 参数。

它支持在多选的时候随意输入的内容作为值,那么这里小编为什么不使用它呢?主要原因是用户输入的值,我们不仅仅作为值,还需要把它做为搜索内容,为此我们才需要单独来实现这块逻辑,让两者的逻辑更可控一点。

滚动加载功能

最后,我们来让分页加载的行为变得多样性一点,让它来支持滚动加载,直接来看代码:

javascript 复制代码
<template>
  <a-select
    ...
    @popupScroll="popupScroll"
  >
    <a-icon @click="clear" slot="clearIcon" type="close-circle" theme="filled" />
    <div slot="dropdownRender" slot-scope="menu">
      <v-nodes :vnodes="menu" />
      <template v-if="loadType === 'pagination'">
        <a-divider class="divider" />
        <div class="paging-box" @mousedown="e => e.preventDefault()">
          <a-pagination
            size="small"
            :current="params.pageNum"
            :pageSize="params.pageSize"
            :total="total"
            @change="paginationChange"
          />
        </div>
      </template>
      <div v-if="loadType === 'scroll' && listData.length >= total && listData.length !== 0" class="empty">暂无更多数据</div>
    </div>
    ...
  </a-select>
</template>

<script>
...
export default {
  ...,
  props: {
    ...
    /** @name 加载方式,pagination/scroll **/
    loadType: {
      type: String,
      default: 'pagination'
    },
    /** @name 分页参数 **/
    pagination: {
      type: Object,
      default: () => ({})
    }
  },
  created() {
    this.params = Object.assign(this.params, this.pagination);
  },
  methods: {
    ...,
    /** @name 点击清除按钮 **/
    clear () {
      this.params = {
        pageNum: 1,
        pageSize: 5,
        keyword: '',
        ...this.pagination // 重置
      }
      this.selectedValue = undefined;
      this.getData();
    },
    /** @name 获取数据 **/
    getData (isScroll) {
      this.loading = true
      getDataAPI(this.params).then(res => {
        this.listData = isScroll ? this.listData = [...this.listData, ...res.rows] : res.rows;
        this.total = res.total
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
    /** @name 下拉列表滚动时的回调 **/
    popupScroll({ target }) {
      if (this.loading || this.total <= this.listData.length || this.loadType !== 'scroll') return;
      const isTouchBottom = (target.scrollTop + target.offsetHeight + 10) > target.scrollHeight
      if (isTouchBottom) {
        this.params.pageNum = this.params.pageNum + 1;
        this.getData(true);
      }
    },
  }
}
</script>

<style scoped>
...
.empty {
  text-align: center;
  font-size: 13px;
  color: #666;
}
</style>

具体使用:

html 复制代码
<template>
  <select-paging v-model="form.scrollValue" loadType="scroll" :pagination="{ pageSize: 10 }" @change="change" />
</template>

写到这里,已经是晚上 22:48 分,累了。

完整源码

点我点我


至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
下雪天的夏风7 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
青稞儿12 分钟前
面试题高频之token无感刷新(vue3+node.js)
vue.js·node.js
diygwcom19 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
Hello-Mr.Wang35 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步7 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者7 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js