el autocomplete支持分页上拉加载使用详解

el-autocomplete使用

  • 效果图

template

<template>
 <el-autocomplete
 :clearable="true" //支持清空
 :title="searchStr" // 鼠标移上去提示文案
 :trigger-on-focus="true" // 聚焦时是否触发下拉列表展示
 :fetch-suggestions="querySearchAsync" // 筛选符合条件的数据
 :placeholder="placeholder" // 占位符提示信息
 v-scrollLoad="load" // 自定义上拉加载指令
 v-model="searchStr" // 搜索关键字
 popper-class="diy-autocomplete" // 下拉框自定义class控制样式
 class="el-autocomplete-component" // 给当前组件定义专属类名
 size="small" // 组件显示尺寸
 ref="autocomplete" // 用于后期获取dom元素
 @select="handleSelect" // 选中时触发事件
 @blur="handleBlur" // 失去焦点时触发
 @clear="handleClear" // 清空数据时触发
 ></el-autocomplete>
</template>

实现需求分析

输入框为空时聚焦或失焦后又重新聚焦不会触发请求数据接口

// blurTxt: 记录上次失焦时 和 选中时的筛选字段
// blurArr: 记录上次失焦时 和 选中时已经查询到的数据
async querySearchAsync(queryString, cb) {
 if (this.blurTxt === queryString || !queryString) {
 cb(this.blurArr)
 return
 }
 },

缓存上一次已查询的数据&搜索条件:blurArr、blurTxt

// 失焦事件
 handleBlur() {
 this.blurTxt = this.searchStr || ''
 this.blurArr = this.$refs['autocomplete'].$data.suggestions
 },
 // 过滤数据时及时更新筛选字段
 async querySearchAsync(queryString, cb) {
 this.blurTxt = searchVal
 },

滚动加载指令(监听容器的scroll事件并进行防抖处理)

  • 防抖函数
/**
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} immediate
 * @return {*}
 */
export function debounce(func, wait, immediate) {
 let timeout, args, context, timestamp, result
 const later = function() {
 // 据上一次触发时间间隔
 const last = +new Date() - timestamp
 // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
 if (last < wait && last > 0) {
 timeout = setTimeout(later, wait - last)
 } else {
 timeout = null
 // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
 if (!immediate) {
 result = func.apply(context, args)
 if (!timeout) context = args = null
 }
 }
 }
 return function(...args) {
 context = this
 timestamp = +new Date()
 const callNow = immediate && !timeout
 // 如果延时不存在,重新设定延时
 if (!timeout) timeout = setTimeout(later, wait)
 if (callNow) {
 result = func.apply(context, args)
 context = args = null
 }
 return result
 }
}
  • 滚动加载指令
directives: {
 scrollLoad: {
 bind(el, binding, vnode) {
 let wrapDom = el.querySelector('.el-autocomplete-suggestion__wrap')
 let listDom = el.querySelector('.el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list')
 // 滚动事件做防抖处理
 wrapDom.addEventListener(
 'scroll',
 debounce(e => {
 let condition = wrapDom.offsetHeight + wrapDom.scrollTop + 50 - listDom.offsetHeight
 if (condition > 0 && !vnode.context.loading) {
 binding.value()
 }
 }, 300),
 false
 )
 }
 }
 }

分页加载

  • 请求前展示加载圈
  • 加载至最后一页时不再进行请求并提示暂无更多数据
  • 关闭loading加载圈
  • 把数据追加至已展示的数据列表中

获取数据,并进行格式化

第一种方式: 在组件上设置valueKey为你要展示的字段名称,默认值为value

<el-autocomplete valueKey="nickName"></el-autocomplete>

第二种方式:拿到数据后遍历数据为每一项增添value属性,值为自己组合想展示的方式

// 获取用户列表
 async getList(queryString) {
 let result = await searchUserList({
 pageNum: this.pageNum,
 pageSize: this.pageSize,
 searchValue: decodeURI(queryString)
 })
 this.total = result.total
 // 调用 callback 返回建议列表的数据
 result.rows &&
 result.rows.forEach(element => {
 // 学生展示 姓名+班级
 if (element.classList[0] && element.roleId === 101) {
 element.value = element.nickName + '-' + element.classList[0].className
 } else {
 // 非学生或者学生没有主班级展示 姓名+身份
 element.value = element.nickName + '-' + (element.roleName || '暂无角色ID')
 }
 })
 return result.rows
 },

第三种方式:在组件对应的插槽slot中自定义展示内容

<el-autocomplete > 
 <!-- 输入框小图标插槽 -->
 <i class="el-icon-edit el-input__icon" slot="suffix"> </i> 
 <!-- 搜索列表每一项展示 -->
 <template slot-scope="{ item }"> 
 <div class="name">{{ item.nickName }} - {{item.className}}</div>
 </template> 
</el-autocomplete>

关闭加载圈

// 关闭加载圈
 closeLoading() {
 loadingInstance && loadingInstance.close && loadingInstance.close()
 loadingInstance = null
 },

分页加载事件

// 滚动加载
 async load() {
 this.closeLoading()
 // 加载到最后一页停止加载
 if (this.pageNum * this.pageSize > this.total) {
 return
 }
 this.pageNum++
 loadingInstance = Loading.service({
 target: document.querySelector('.el-autocomplete-suggestion'),
 fullscreen: false,
 spinner: 'el-icon-loading',
 lock: true,
 text: '加载中...'
 })
 let results = await this.getList(this.searchStr)
 this.closeLoading()
 this.pageNum * this.pageSize >= this.total ? results.push({ value: '暂无更多数据' }) : ''
 // 将数据添加到下拉列表
 this.$refs['autocomplete'].$data.suggestions = this.$refs['autocomplete'].$data.suggestions.concat(results)
 },

清空输入框,重置上次记录的数据

// 清空搜索项
 handleClear() {
 this.blurTxt = ''
 this.blurArr = []
 this.$refs['autocomplete'].$data.suggestions = []
 },

选中时记录相关数据

// 选中用户跳转至对应的页面
 handleSelect(item) {
 this.$refs['autocomplete'].$data.suggestions = this.blurArr = [item]
 this.blurTxt = this.searchStr || ''
 this.pageNum = 1
 this.total = 0
 ...
 //下拉选中的值
 // console.log(item)
 }

数据展示不稳定问题

例如姓名模糊搜索过程中,也许我们会先输入姓为第一个关键词,接着在输入第二个关键词名字,只输入姓的时候肯定要比姓名要查询的数据多,当在大量数据中查询时会面临着第二个请求(搜索条件:输入姓名的)先返回数据,然后第一个请求(搜索条件:输入姓的)才会返回数据的情况,而此时筛选列表中展示的肯定是最后请求出来的结果(搜索框中展示的是完整姓名:张三,而展示列表中却展示出了:张一、张二、张三...),此时的解决方案是相同接口取消上一次的接口。

  • 请求拦截中限制重复请求某个接口
import axios from 'axios'
let pending = []; //声明一个数组用于存储每个ajax请求的取消函数和ajax标识
let cancelToken = axios.CancelToken;
let removePending = (ever) => {
 for (let p in pending) {
 if (pending[p].u === ever.url + '&' + ever.method) { //当当前请求在数组中存在时执行函数体 
 pending[p].f(); //执行取消操作
 pending.splice(p, 1); //把这条记录从数组中移除
 }
 }
}
var errorFlag = false;
var erFlag = false;
// 创建axios实例
const service = axios.create({
 // axios中请求配置有baseURL选项,表示请求URL公共部分
 baseURL: process.env.VUE_APP_BASE_API,
 // 超时
 timeout: 90000
})
// request拦截器
service.interceptors.request.use(
 config => {
 // 如果你是在老项目中开发就加一个限制,避免影响到原有的功能
 // if(config.url.indexOf('system/user/newsearch_list')!==-1){
 config && removePending(config); //在一个ajax发送前执行一下取消操作
 config.cancelToken = new cancelToken((c) => {
 // 这里的ajax标识我是用请求地址&请求方式拼接的字符串,当然你可以选择其他的一些方式
 pending.push({
 u: config.url + '&' + config.method,
 f: c
 });
 });
 // }
 return config
 },
 error => {
 console.log(error)
 Promise.reject(error)
 }
)
  • 相应拦截中对取消请求这个操作单独处理,不展示错误消息提示弹窗
// 响应拦截器
service.interceptors.response.use(res => {
 const code = res.data.code
 if (code === 401) {
 ... 
 } else if (code !== 200) {
 if(!errorFlag){
 ... 
 return Promise.reject(res.data || {})
 } 
 } else {
 return res.data
 }
 },
 error => {
 // 单独处理取消请求导致的错误
 if(error.__CANCEL__){
 return false
 }
 if(!erFlag){
 Message({
 message: error.message,
 type: 'error',
 duration: 3 * 1000
 })
 return Promise.reject(error)
 } 
 }
)

完整的 scss 文件

.el-autocomplete-component {
 max-width: 230px;
 vertical-align: text-bottom;
 height: 50px;
 padding-top: 1px;
 cursor: pointer;
 /deep/ .el-input__inner {
 cursor: pointer;
 padding-left: 5px;
 padding-right: 8px;
 background: transparent;
 border: none;
 color: #fff;
 font-size: 14px;
 text-overflow: ellipsis;
 white-space: nowrap;
 overflow: hidden;
 &::placeholder {
 color: #bfbfbf;
 font-size: 12px;
 }
 }
}
.diy-autocomplete {
 .name {
 max-width: 180px;
 overflow: hidden;
 text-overflow: ellipsis;
 white-space: nowrap;
 height: 34px;
 }
}

完整的 js 文件

<script>
import { searchUserList } from '@/api/system/user' // 请求用户列表的接口
import { debounce } from '@/utils/index' // 防抖函数
import { Loading } from 'element-ui' // 下拉加载时的过渡loading
let loadingInstance = null
export default {
 data() {
 return {
 showAutocomplete: false,
 searchStr: '', //输入关键词的值
 pageNum: 1,
 pageSize: 20,
 total: 0, //筛选数据的总值
 placeholder: '请输入用户名/手机号/QQ',
 blurTxt: '', //记录失焦时搜索框中的文字,避免聚焦时重新筛选数据
 blurArr: [] //记录失焦时已经搜索出来的列表
 }
 },
 methods: {
 // 失焦事件
 handleBlur() {
 this.blurTxt = this.searchStr || ''
 this.blurArr = this.$refs['autocomplete'].$data.suggestions
 },
 // 清空搜索项
 handleClear() {
 this.blurTxt = ''
 this.blurArr = []
 this.$refs['autocomplete'].$data.suggestions = []
 },
 // 关闭加载圈
 closeLoading() {
 loadingInstance && loadingInstance.close && loadingInstance.close()
 loadingInstance = null
 },
 // 条件查询
 async querySearchAsync(queryString, cb) {
 this.$refs['autocomplete'].$data.suggestions = []
 if (this.blurTxt === queryString || !queryString) {
 cb(this.blurArr)
 return
 }
 this.handleClear()
 let searchVal = queryString
 // 后面所拼接的班级名称和角色不参与筛选字段中
 queryString.indexOf('-') !== -1 ? (searchVal = queryString.split('-')[0]) : ''
 this.pageNum = 1
 this.blurTxt = searchVal
 let results = await this.getList(searchVal)
 cb(results || [])
 },
 // 获取用户列表
 async getList(queryString) {
 let result = await searchUserList({
 pageNum: this.pageNum,
 pageSize: this.pageSize,
 searchValue: decodeURI(queryString)
 })
 this.total = result.total
 // 调用 callback 返回建议列表的数据
 result.rows &&
 result.rows.forEach(element => {
 // 学生展示 姓名+班级
 if (element.classList[0] && element.roleId === 101) {
 element.value = element.nickName + '-' + element.classList[0].className
 } else {
 // 非学生或者学生没有主班级展示 姓名+身份
 element.value = element.nickName + '-' + (element.roleName || '暂无角色ID')
 }
 })
 return result.rows
 },
 // 滚动加载
 async load() {
 this.closeLoading()
 // 加载到最后一页停止加载
 if (this.pageNum * this.pageSize > this.total) {
 return
 }
 this.pageNum++
 loadingInstance = Loading.service({
 target: document.querySelector('.el-autocomplete-suggestion'),
 fullscreen: false,
 spinner: 'el-icon-loading',
 lock: true,
 text: '加载中...'
 })
 let results = await this.getList(this.searchStr)
 this.closeLoading()
 this.pageNum * this.pageSize >= this.total ? results.push({ value: '暂无更多数据' }) : ''
 // 将数据添加到下拉列表
 this.$refs['autocomplete'].$data.suggestions = this.$refs['autocomplete'].$data.suggestions.concat(results)
 },
 // 选中用户跳转至对应的页面
 handleSelect(item) {
 this.$refs['autocomplete'].$data.suggestions = this.blurArr = [item]
 this.blurTxt = this.searchStr || ''
 this.pageNum = 1
 this.total = 0
 let routeData = {}
 if (item.roleId === 101) {
 // 学生
 routeData = this.$router.resolve({ path: '/personInf/student', query: { userId: item.userId } })
 } else {
 // 非学生
 routeData = this.$router.resolve({
 path: '/userManagement/user',
 query: { userInfo: item.nickName ,roleId: item.roleId||''}
 })
 }
 window.open(routeData.href, '_blank')
 //下拉选中的值
 // console.log(item)
 }
 },
 directives: {
 scrollLoad: {
 bind(el, binding, vnode) {
 let wrapDom = el.querySelector('.el-autocomplete-suggestion__wrap')
 let listDom = el.querySelector('.el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list')
 // 滚动事件做防抖处理
 wrapDom.addEventListener(
 'scroll',
 debounce(e => {
 let condition = wrapDom.offsetHeight + wrapDom.scrollTop + 50 - listDom.offsetHeight
 if (condition > 0 && !vnode.context.loading) {
 binding.value()
 }
 }, 300),
 false
 )
 }
 }
 }
}
</script>
作者:日升_月落

%s 个评论

要回复文章请先登录注册