上拉下拉无限滚动组件-pc端

Posted fan-zha

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了上拉下拉无限滚动组件-pc端相关的知识,希望对你有一定的参考价值。

场景:

web项目,聊天记录历史搜索。需要支持上拉无限加载,下拉无限加载。

目标:

支持所需场景;可配置。

难点:

顶部无限滚动很麻烦,网上没找着好的解决方案。刚开始 顶部也想使用 IntersectionObserver 特性来做,但二次触发比较麻烦,后来改用监听 scroll 事件。问题又来了,滚动条一直处于顶部,无法保持原来的位置。

本例解决方案是:利用 scrollIntoViewIfNeeded 特性,在组装列表完成后,手动调用,使其滚动到 原来的列表项位置。

成品效果:

凑合能用,O(∩_∩)O哈哈~

技术图片

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="//unpkg.com/vue/dist/vue.js"></script>

    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/lodash@4.17.4/lodash.min.js"></script>

    <style>
        body, html {
            margin: 0;
            padding: 0;
            height: 100%;
        }
        .scroll-container {
            height: 200px;
            overflow-y: scroll;
            background: red;
        }
        .scroll-container li {
            height: 50px;
        }
    </style>
</head>
<body>
<div id="app">
    <template>
        <tf-scroll ref="scrollHandle"
                   list-item-dom-handle="li"
                   :is-scroll-to-bottom="scrollOptions.isScrollToBottom"
                   :top-observer-visible="scrollOptions.topObserverVisible"
                   :bottom-observer-visible="scrollOptions.bottomObserverVisible"
                   loading-text="加载呢"
                   no-more-text="麻溜停,没了"
                   @load-more-up="loadMoreUp"
                   @load-more-down="loadMoreDown">
            <ul>
                <li :class="{‘is-top‘: index == 3}" class="list-item" v-for="(item,index) in tableData">{{item.userName}}</li>
            </ul>
        </tf-scroll>
    </template>
</div>
<script>
    var Main = {
        data() {
            return {
                scrollOptions: {
                    isScrollToBottom: false,
                    topObserverVisible: true,
                    bottomObserverVisible: true,
                },

                addCountUp: 0,
                addCountDown: 0,
                tableData: []
            }
        },
        components: {
            /**
             * 上拉、下拉无限滚动组件
             * 支持场景:
             * 1、滚动至底部无限加载
             * 2、滚动至顶部无限加载
             * 3、1与2场景共存
             *
             * 配置项:
             * 1、is-scroll-to-bottom {boolean=false} 是否需要滚动到底部
             * 2、top-observer-visible {boolean=false} 是否启用底部无限加载功能
             * 3、bottom-observer-visible {boolean=true} 是否启用顶部无限加载功能
             * 4、loading-text {string=拼命加载中} 数据加载中文本
             * 5、no-more-text {string=没有更多数据} 没有更多文本
             * 6、list-item-dom-handle {string} 列表项元素选择符;top-observer-visible 为真时,需要提供
             * 7、load-more-up {event} 顶部无限加载广播事件名
             * 8、load-more-down {event} 底部无限加载广播事件名
             *
             * 使用过程注意事项:
             * 1、初始化: 父组件在 mounted 函数里,进行列表获取后,调用当前组件的init事件。
             * 2、启用底部无限加载功能时:如果列表没有更多时,需要手动调用组件的 endObserveDown 事件。
             * 3、启用顶部无限加载功能时:
             *      a、如果列表没有更多时,需要手动调用组件的 endObserveUp 事件。
             *      b、每次获取数据成功后,需要调用组件的 endLoadingUp 事件,用以结束loading状态。
             *
            */
            tfScroll: {
                template: `
                <div ref="scrollHandle" @scroll.native="intersectedUp" class="scroll-container">
                    <div style="display: flex; align-items: center; justify-content: center; height: 60px;" v-if="topObserverVisible">{{statusText}}</div>
                    <slot></slot>
                    <sentinels-observer ref="downSentinelsHandle" v-if="bottomObserverVisible"
                    :loading-text="loadingText"
                    :no-more-text="noMoreText"
                    @intersect="intersectedDown"></sentinels-observer>
                </div>
`,
                components: {
                    sentinelsObserver: {
                        template: `
             <div style="display: flex; align-items: center; justify-content: center; height: 60px;">{{statusText}}</div>
            `,
                        props: {
                            options: Object,
                            loadingText: {
                                default: 拼命加载中,
                                type: String
                            },
                            noMoreText: {
                                default: 没有更多数据,
                                type: String
                            }
                        },
                        data: () => ({statusText: ‘‘, observer: null}),
                        mounted() {
                            this.statusText = this.loadingText
                        },
                        methods: {
                            startObserve () {
                                const options = this.options || {};
                                this.observer = new IntersectionObserver(([entry]) => {
                                    if (entry && entry.isIntersecting) {
                                        this.$emit(intersect);
                                    }
                                }, options);
                                this.observer.observe(this.$el);
                            },
                            removeObserve () {
                                this.statusText = this.noMoreText
                                this.observer.disconnect();
                            }
                        },
                        destroyed() {
                            this.observer.disconnect();
                        }
                    }
                },
                props: {
                    listItemDomHandle: {
                        type: String
                    },
                    isScrollToBottom: {
                        default: false,
                        type: Boolean
                    },
                    topObserverVisible: {
                        default: false,
                        type: Boolean
                    },
                    bottomObserverVisible: {
                        default: true,
                        type: Boolean
                    },
                    loadingText: {
                        default: 拼命加载中,
                        type: String
                    },
                    noMoreText: {
                        default: 没有更多数据,
                        type: String
                    },
                },
                data() {
                    return {
                        topLoading: false,
                        topNoMore: false,
                    }
                },
                created () {
                    this.scrollEvent = _.debounce(this.intersectedUp, 1000);
                },
                computed: {
                    statusText () {
                        if (this.topNoMore)
                            return this.noMoreText
                        return this.loadingText
                    }
                },
                methods: {
                    /*
                    初始化:第一次读取数据后,手动调用;
                     */
                    init () {
                        this.topLoading = false;
                        this.topNoMore = false;
                        this.$nextTick(() => {
                            if (this.isScrollToBottom)
                                this.$refs.scrollHandle.scrollTo(0, this.$refs.scrollHandle.scrollHeight);
                            if (this.topObserverVisible) {
                                if (!this.listItemDomHandle)
                                    throw 当前模式支持下拉滚动,请提供列表项元素选择符;
                                let doms = this.$refs.scrollHandle.querySelectorAll(this.listItemDomHandle);
                                doms[Math.floor(doms.length/2)].scrollIntoViewIfNeeded();
                                this.$refs.scrollHandle.addEventListener(scroll, this.scrollEvent)
                            }
                            if (this.bottomObserverVisible)
                                this.$refs.downSentinelsHandle.startObserve();
                        })
                    },
                    intersectedUp () {
                        // 顶部滚动事件:60 为 顶部观察器的元素高度;手动指定的滚动距离需要大于 当前元素的已滚动高度;
                        if (this.$refs.scrollHandle.scrollTop <= 60*2 && !this.topNoMore) {
                            this.$refs.scrollHandle.scrollTo(0, 60*3)
                            if (!this.topLoading) {
                                console.log(上方-滚动)
                                this.topLoading = true;
                                this.$emit(load-more-up); // 父组件 在获取成功后 需要将topLoading 置为 false
                            }
                        }
                    },
                    intersectedDown() {
                        console.log(下方-观察器出现,注意隐蔽)
                        this.$emit(load-more-down);
                    },
                    endLoadingUp (res) {
                        this.$nextTick(() => this.$refs.scrollHandle.querySelectorAll(this.listItemDomHandle)[res.length].scrollIntoViewIfNeeded());
                        this.topLoading = false;
                    },
                    endObserveUp () {
                        this.topNoMore = true;
                        this.topLoading = false;
                    },
                    endObserveDown () {
                        this.$refs.downSentinelsHandle.removeObserve()
                    }
                }
            }
        },
        mounted () {
            this.tableData = [
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1575356523532",
                      "messageType": 0,
                      "body": {"msg": ""},
                      "userName": "0郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 1,
                      "body": {
                          "name": "图片发送于2015-05-07 13:59",         //图片name
                          "md5": "9894907e4ad9de4678091277509361f7",   //图片文件md5
                          "url": "http://imgwx5.2345.com/dypcimg/drama/img/role/0/53208/xiaotingsheng.jpg",    //生成的url
                          "ext": "jpg",        //图片后缀
                          "w": 6814,       //
                          "h": 2332,       //
                          "size": 388245       //图片大小
                      },
                      "userName": "1郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 3,
                      "body": {
                          "dur": 8003,     //视频持续时长ms
                          "md5": "da2cef3e5663ee9c3547ef5d127f7e3e",   //md5
                          "url": "http://nimtest.nos.netease.com/21f34447-e9ac-4871-91ad-d9f03af20412",    //生成的url
                          "w": 360,    //
                          "h": 480,    //
                          "ext": "mp4",    //视频格式
                          "size": 16420    //视频文件大小
                      },
                      "userName": "2郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 6,
                      "body": {
                          "name": "BlizzardReg.ttf",   //文件名
                          "md5": "79d62a35fa3d34c367b20c66afc2a500", //文件MD5
                          "url": "http://nimtest.nos.netease.com/08c9859d-183f-4daa-9904-d6cacb51c95b", //文件URL
                          "ext": "ttf",    //文件后缀类型
                          "size": 91680    //大小
                      },
                      "userName": "3郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 9,
                      "body": "{"msg":"恩"}",
                      "userName": "4郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 1,
                      "body": {
                          "name": "图片发送于2015-05-07 13:59",         //图片name
                          "md5": "9894907e4ad9de4678091277509361f7",   //图片文件md5
                          "url": "http://imgwx3.2345.com/dypcimg/drama/img/role/0/53208/linxi.jpg",    //生成的url
                          "ext": "jpg",        //图片后缀
                          "w": 6814,       //
                          "h": 2332,       //
                          "size": 388245       //图片大小
                      },
                      "userName": "5郭劭杰",
                      "businessRowNO": 1
                  },
                  {
                      "favicon": "686481",
                      "OID": "a39a6dbdb954442dac9f6b5dd2791f16",
                      "sendtime": "1555659541837",
                      "messageType": 3,
                      "body": {
                          "dur": 8003,     //视频持续时长ms
                          "md5": "da2cef3e5663ee9c3547ef5d127f7e3e",   //md5
                          "url": "https://www.w3school.com.cn/i/movie.ogg",    //生成的url
                          "w": 360,    //
                          "h": 480,    //
                          "ext": "mp4",    //视频格式
                          "size": 16420    //视频文件大小
                      },
                      "userName": "6郭劭杰",
                      "businessRowNO": 1
                  },
              ];
            this.$refs.scrollHandle.init();
        },
        methods: {
            loadMoreUp () {
                this.loadData(true).then(res => {
                    res.forEach(item => this.tableData.unshift(item));
                    this.$refs.scrollHandle.endLoadingUp(res)
                }).catch(() => {
                    this.$refs.scrollHandle.endObserveUp()
                });
            },
            loadMoreDown () {
                this.loadData().then(res => {
                    res.forEach(item => this.tableData.push(item))
                }).catch(() => this.$refs.scrollHandle.endObserveDown());
            },

            loadData (isUp) {
                let namePre = ‘‘;
                if (isUp) {
                    this.addCountUp ++;
                    namePre = `up${this.addCountUp}`
                }
                else {
                    this.addCountDown ++;
                    namePre = `down${this.addCountDown}`
                }

                if (isUp && this.addCountUp > 2)
                    return Promise.reject();
                else if (this.addCountDown > 2)
                    return Promise.reject();
                return this.mockData([
                    {userName: `${namePre}-0`}, {userName: `${namePre}-1`}, {userName: `${namePre}-2`},
                    {userName: `${namePre}-3`}, {userName: `${namePre}-4`}, {userName: `${namePre}-5`},
                    {userName: `${namePre}-6`}, {userName: `${namePre}-7`}, {userName: `${namePre}-8`},
                ])
            },
            mockData (data) {
                return new Promise((resolve, reject) => {
                    setTimeout(function () {
                        resolve(data)
                    }, 500)
                });
            }
        }
    }
    var Ctor = Vue.extend(Main)
    new Ctor().$mount(#app)
</script>
</body>
</html>

 

以上是关于上拉下拉无限滚动组件-pc端的主要内容,如果未能解决你的问题,请参考以下文章

vue中好用的下拉刷新、上拉加载插件mescroll.js

vue10行代码实现上拉翻页加载更多数据,纯手写js实现下拉刷新上拉翻页不引用任何第三方插件

基于 Vue.js 的移动端组件库mint-ui实现无限滚动加载更多

基于 Vue.js 的移动端组件库mint-ui实现无限滚动加载更多

[小黄书小程序]主页面笔记图片高度自适应及上拉无限加载及下拉更新

uniapp实现下拉刷新及上拉(分页)加载更多(app,H5,小程序均可使用)