vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue el-select无限滚动

Vue二次封装el-select实现下拉滚动加载效果(el-select无限滚动)

作者:Tencent IT

el-select默认是不支持虚拟滚动的,需要使用第三方插件来实现虚拟滚动功能,下面这篇文章主要给大家介绍了关于Vue二次封装el-select实现下拉滚动加载效果的相关资料,需要的朋友可以参考下

前言

平时我们做业务需求的时候,可能会遇到非常大量的数据,有时候成百上千条,一般后端都会写一个分页的接口,只要我们请求的时候加上页码参数即可。

但是在使用element-ui的el-select下拉菜单组件中,官方没有提供相应的方法进行多页加载。

这时候我们可以实现一个Vue的自定义指令,每当使用el-select滚动到列表底部的时候就请求下一页数据,来达到下拉滚动加载更多的目的。

实现自定义指令

首先实现一个el-select下拉加载的自定义指令v-loadmore:

// directives.js
import Vue from 'vue'

Vue.directive("loadmore", {
    bind(el, binding, vnode) {
        const SELECTWRAP = el.querySelector(
            ".el-select-dropdown .el-select-dropdown__wrap"
        );
        SELECTWRAP.addEventListener("scroll", function () {
            // scrollTop 这里可能因为浏览器缩放存在小数点的情况,导致了滚动到底部时
            // scrollHeight 减去滚动到底部时的scrollTop ,依然大于clientHeight 导致无法请求更多数据
            // 这里将scrollTop向上取整 保证滚到底部时,触发调用
            const CONDITION = this.scrollHeight - Math.ceil(this.scrollTop) <= this.clientHeight;
            // el.scrollTop !== 0 当输入时,如果搜索结果很少,以至于没看到滚动条,那么此时的CONDITION计算结果是true,会执行bind.value(),此时不应该执行,否则搜索结果不匹配
            if (CONDITION && this.scrollTop !== 0) {
                binding.value();
            }
        });
    },
});

代码说明:

document.querySelector:querySelector() 方法仅仅返回匹配指定选择器的第一个元素。

Element.scrollHeight:在不使用滚动条的情况下为了适应视口中所用内容所需的最小高度(只读)

警告: 在使用显示比例缩放的系统上,scrollTop可能会提供一个小数。

Element.scrollTop:获取或设置一个元素的内容垂直滚动的像素数。

Element.clientHeight:读取元素的可见高度(只读)。

如果元素滚动到底,下面等式返回true,没有则返回false。

// scrollTop 这里可能因为浏览器缩放不等于100%时,存在小数点的情况,导致了滚动到底部时

// scrollHeight 减去滚动到底部时的scrollTop ,依然大于clientHeight 导致没有触发加载事件

// 这里将scrollTop向上取整 保证滚到底部时,触发调用

// 此判断不准确: element.scrollHeight - element.scrollTop === element.clientHeight// 使用下面的判断方式保证

任何缩放都能触发:

element.scrollHeight - Math.ceil(element.scrollTop) <= element.clientHeight

在项目中全局注册v-loadmore指令:

// main.js

import directives from './directive.js'
Vue.use(directives)

最后在组件el-select中使用该指令:

<template>
    <el-select v-model="selected" v-loadmore="loadMore">
        <el-option
            v-for="option in options"
            :label="option.label"
            :value="option.value"
            :key="option.value"
        ></el-option>
    </el-select>
</template>

<script>
export default {
    data() {
        return {
            selected: "",
            options: [
                {
                    label: "1",
                    value: 1
                },
                // ... 此处省略多个选项
                {
                    label: "到达底部啦",
                    value: 9
                }
            ]
        };
    },
    methods: {
        loadMore() {
            console.log("more")
        }
    }
};

使用效果如下:

从效果图可以看出,每当菜单列表滚动到底部时,指令就会调用传入的loadMore函数,控制台随即打印出 “more”。
注意事项:

传入的数组个数必须大于或者等于8个选项时才能让el-select组件出现下拉滚动。
列表里不存在滚动时,无法触发传入指令的函数。

进行二次封装

滚动到底部调用函数的指令已经实现了,下面只要调用接口,把获取到下一页的数据拼接到当前的数据中即可。

接下来把el-select进行二次封装,封装成公用的组件之后,传入必要的参数就可以在项目中调用。

首先新建一个文件load-select.vue:

<template>
    <el-select :value="value" v-loadmore="loadMore" @focus="focus" v-bind="$attrs" v-on="$listeners">
        <el-option
            v-for="option in data"
            :label="option[dictLabel]"
            :value="option[dictValue]"
            :key="option.value"
        ></el-option>
    </el-select>
</template>

<script>
export default {
    props: {
        value: {
            type: String,
            default: ""
        },
        // 列表数据
        data: {
            type: Array,
            default: () => []
        },
        dictLabel: {
            type: String,
            default: "label"
        },
        dictValue: {
            type: String,
            default: "value"
        },
        // 调用页数的接口
        request: {
            type: Function,
            default: () => {}
        },
        page: {
            type: [Number, String],
            default: 1
        }
    },
    data() {
        return {};
    },
    methods: {
        // 请求下一页的数据
        loadMore() {
            this.request({ page: this.page + 1 })
        },
        // 选中下拉框没有数据时,自动请求第一页的数据
        focus() {
            if (!this.data.length) {
                this.request({page: 1})
            }
        }
    }
};
</script>

在页面组件中调用load-select.vue:

<!-- page.vue -->

<template>
    <div class="xxx-page">
        <load-select v-model="selected" :data="data" :page="page" :request="getData"></load-select>
    </div>
</template>

<script>
// 导入该组件
import loadSelect from "@/components/load-select/index";

export default {
    name: "app",
    components: {
        loadSelect
    },
    data() {
        return {
            selected: "",
            page: 1,
            more: true,
            data: []
        };
    },
    methods: {
        // 传入给load-select组件的函数
        getData({ page = 1 } = {}) {
            // 输出页数
            console.log(page)
            // 访问后端接口API
            this.requestAPI({ page }).then(res => {
                this.data = [...this.data, ...res.result]
                this.page = res.page
            });
        },
        // 模拟后端接口的API
        requestAPI({ page = 1, size = 10 } = {}) {
            return new Promise(resolve => {
                let responseData = []
                // 假设总共的数据有50条
                let total = 50; 
                for (let index = 1; index <= size; index++) {
                    // serial:处于第几个元素,就显示多少序号
                    let serial = index + (page - 1) * size
                    if (serial <= 50) {
                        responseData.push({
                            label: serial,
                            value: serial
                        });
                    }
                }
                // 模拟异步请求,500ms之后返回接口的数据
                setTimeout(() => {
                    resolve({
                        total,
                        page,
                        size,
                        result: responseData
                    });
                }, 500);
            });
        }
    }
};
</script>

代码解析:

首次点击下拉框时,会触发focus事件请求第一页的数据,之后只要每次滚动列表到底部,就会自动请求下一页的数据然后拼接到当前的数组中。

我们来看看效果:

完美!但是在实际使用的过程中,可能会因为接口还来不及返回数据,然后列表又向下滚动再次触发了请求,结果就是返回了两份相同的数据。

现在把接口的延迟调到2000ms重现这个场景:

在两次快速滚动到底部的时候,请求的参数页数都是2,如何解决这个问题?可以在加载函数中加入一个拦截操作,在接口没有响应之前,不调用加载函数,不过这样做要把getData转换成异步函数的形式。

首先在load-select.vue中的loadMore()中加入一个拦截操作:

<!-- load-select.vue -->

<template>
    ...
</template>

<script>
    // 请求下一页的数据
    methods: {
        loadMore() {
            // 如果 intercept 属性为 true 则不请求数据
            if (this.loadMore.intercept) {
                return 
            }
            this.loadMore.intercept = true
            this.request({ page: this.page + 1 }).then(() => {
                // 接口响应之后才把 intercept 设置为 false
                this.loadMore.intercept = false
            })
        }
    }
</script>

然后在page.vue中的getData()函数转换成异步函数的形式:

<template>
    ...
</template>

<script>
    methods: {
        // 传入给load-select组件的函数
        getData({ page = 1 } = {}) {
            // 返回 Promise 对象
            return new Promise( resolve => {
                // 访问后端接口API
                this.requestAPI({ page }).then(res => {
                    this.data = [...this.data, ...res.result]
                    this.page = res.page
                    resolve()
                });
            })
        }, 
    
    }
</script>

现在问题来了:

一般分页的接口都支持关键字的搜索,load-select.vue组件能不能加入关键字搜索的功能呢?

关键字搜索功能

还好el-select组件支持远程搜索功能,只要传入filterable和remote参数,具体的可以查看element-ui的官方文档。

接下来对load-select.vue进行以下修改:

<!-- load-select.vue -->
<template>
    <el-select
        :value="value"
        v-loadmore="loadMore"
        @focus="focus"
        filterable
        remote
        :filter-method="handleSearch"
        :loading="loading"
        clearable
        v-bind="$attrs"
        v-on="$listeners"
    >
        <el-option
            v-for="option in data"
            :label="option[dictLabel]"
            :value="option[dictValue]"
            :key="option.value"
        ></el-option>
        <!-- 此处加载中的value可以随便设置,只要不与其他数据重复即可 -->
        <el-option v-if="hasMore" disabled label="加载中..." value="-1"></el-option>
    </el-select>
</template>

<script>
export default {
    props: {
        value: {
            default: ""
        },
        // 列表数据
        data: {
            type: Array,
            default: () => []
        },
        dictLabel: {
            type: String,
            default: "label"
        },
        dictValue: {
            type: String,
            default: "value"
        },
        // 调用页数的接口
        request: {
            type: Function,
            default: () => {}
        },
        // 传入的页码
        page: {
            type: [Number, String],
            default: 1
        },
        // 是否还有更多数据
        hasMore: {
            type: Boolean,
            default: true
        }
    },
    data() {
        return {
            // 存储关键字用
            keyword: "", 
            loading: false
        };
    },
    methods: {
        // 请求下一页的数据
        loadMore() {
            // 如果没有更多数据,则不请求
            if (!this.hasMore) {
                return
            }
            // 如果intercept属性为true则不请求数据,
            if (this.loadMore.intercept) {
                return
            }
            this.loadMore.intercept = true;
            this.request({
                page: this.page + 1,
                more: true,
                keyword: this.keyword
            }).then(() => {
                this.loadMore.intercept = false
            });
        },
        // 选中下拉框没有数据时,自动请求第一页的数据
        focus() {
            if (!this.data.length) {
                this.request({ page: 1 })
            }
        },
        // 关键字搜索
        handleSearch(keyword) {
            this.keyword = keyword
            this.loading = true
            this.request({ page: 1, keyword }).then(() => {
                this.loading = false
            });
        },
        // 删除选中时,如果请求了关键字,则清除关键字再请求第一页的数据
        clear() {
            if (this.keyword) {
                this.keyword = ""
                this.request({ page: 1 })
            }
        }
    }
};
</script>

页面调用时,getData()请求函数需要接收keyword和more参数并进行相应的处理:

<!-- page.vue -->

<template>
    <div class="xxx-page">
        <load-select v-model="selected" :data="data" :page="page" :hasMore="more" :request="getData"></load-select>
    </div>
</template>

<script>
// 导入该组件
import loadSelect from "@/components/load-select/index";

export default {
    name: "app",
    components: {
        loadSelect
    },
    data() {
        return {
            selected: "",
            page: 1,
            more: true,
            data: []
        };
    },
    methods: {
        // 传入给load-select组件的函数
        getData({ page = 1, more = false, keyword = "" } = {}) {
            return new Promise(resolve => {
                // 访问后端接口API
                this.requestAPI({ page, keyword }).then(res => {
                    // 如果是加载更多,则合并之前的数据
                    if (more) {
                        this.data = [...this.data, ...res.result]
                    } else {
                        this.data = res.result
                    }

                    this.page = res.page;
                    let { total, page, size } = res
                    // 如果为最后一页,则设置more为false
                    this.more = page * size < total
                    this.page = page
                    resolve()
                });
            });
        },
        // 模拟后端接口的API
        requestAPI({ page = 1, size = 10, keyword = "" } = {}) {
            return new Promise(resolve => {
                // 如果有 keyword 参数,则返回带有 keyword 的数据
                if (keyword) {
                    setTimeout(() => {
                        resolve({
                            total: 3,
                            page: 1,
                            size: 10,
                            result: [
                                {
                                    label: keyword,
                                    value: 1
                                },
                                {
                                    label: keyword + 1,
                                    value: 2
                                },
                                {
                                    label: keyword + 2,
                                    value: 3
                                }
                            ]
                        })
                    }, 500)
                    return
                }

                let responseData = [];
                // 假设总共的数据有50条
                let total = 50; 
                for (let index = 1; index <= size; index++) {
                    // serial:处于第几个元素,就显示多少序号
                    let serial = index + (page - 1) * size
                    if (serial <= 50) {
                        responseData.push({
                            label: serial,
                            value: serial
                        });
                    }
                }
                setTimeout(() => {
                    resolve({
                        total,
                        page,
                        size,
                        result: responseData
                    })
                }, 500)
            })
        }
    }
};
</script>

接下来看看搜索关键字的效果:

搜索功能也完成啦!

总结

为了适用于大部分的请求接口,因此在设计这个组件的时候只能把请求与组件剥离开来,易用程度不算太高,不过我们可以适当地传入一些简单必要的参数去维持基本地使用。

当然,在项目中遇到某些固定的加载请求时,我们也可以对该组件进行再次封装,具体可以根据自身的业务需求进行修改。

到此这篇关于Vue二次封装el-select实现下拉滚动加载效果(el-select无限滚动)的文章就介绍到这了,更多相关Vue el-select无限滚动内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文