| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 | <template>    <view class="scroll-list-wrap" :style="[scrollListWrapStyle]">        <scroll-view            class="scroll-view"            :class="[elClass]"            :style="[listWrapStyle]"            scroll-y            scroll-anchoring            enable-back-to-top            :scroll-top="scrollTop"            :lower-threshold="defaultOption.lowerThreshold"            @scroll="handleScroll"            @touchend="handleTouchEnd"            @touchmove.prevent.stop="handleTouchMove"            @touchstart="handleTouchStart"            @scrolltolower="handleScrolltolower"        >            <view class="scroll-content" :style="[scrollContentStyle]">                <view class="pull-down-wrap">                    <slot name="pulldown" v-if="$slots.pulldown"></slot>                    <view class="refresh-view" :style="[refreshViewStyle]" v-else>                        <view class="pull-down-animation" :class="{ refreshing: refreshing }" :style="[pullDownAnimationStyle]"></view>                        <text class="pull-down-text" :style="[pullDownTextStyle]">{{ refreshStateText }}</text>                    </view>                </view>                <view class="empty-wrap-me" v-if="showEmpty">                    <slot name="empty" v-if="$slots.empty"></slot>                    <view class="empty-view-me" v-else>                        <image class="empty-image" :src="defaultOption.emptyImage || images.empty" mode="aspectFit"></image>                        <text class="empty-text" :style="[emptyTextStyle]">{{ emptyText }}</text>                    </view>                </view>                <view class="list-content"><slot></slot></view>                <view class="pull-up-wrap" v-if="showPullUp">                    <slot name="pullup" v-if="$slots.pullup"></slot>                    <view class="load-view" v-else>                        <view class="pull-up-animation" v-if="loading" :style="[pullUpAnimationStyle]"></view>                        <text class="pull-up-text" :style="[pullUpTextStyle]">{{ loadStateText }}</text>                    </view>                </view>            </view>        </scroll-view>    </view></template><script>import images from './images.js';export default {    name: 'scroll-list',    props: {        // 配置信息        option: {            type: Object,            default: () => ({})        }    },    data() {        return {            defaultOption: {                page: 1, // 分页                size: 10, // 分页大小                auto: true, // 自动加载                height: null, // 组件高度                disabled: false, // 禁用                background: '', // 背景颜色属性                emptyImage: '', // 空数据提示图片                offsetBottom: 0, // 底部高度补偿                pullDownSpeed: 0.5, // 下拉速率                lowerThreshold: 40, // 距离底部上拉加载距离                refresherThreshold: 80, // 距离顶部下拉刷新距离                refreshDelayed: 800, // 刷新延迟                refreshFinishDelayed: 800, // 刷新完成后的延迟                safeArea: false, // 是否开启安全区域适配                emptyTextColor: '#82848a', // 空提示文字颜色                loadTextColor: '#82848a', // 上拉加载文字颜色                loadIconColor: '#82848a', // 上拉加载图标颜色                refresherTextColor: '#82848a', // 下拉刷新文字颜色                refresherIconColor: '#82848a', // 下拉刷新图标颜色                emptyText: '暂无列表~', // 空数据提示文字                loadingText: '正在加载中~', // 加载中文字                loadFailText: '加载失败啦~', // 加载失败文字                noMoreText: '没有更多啦~', // 没有更多文字                refreshingText: '正在刷新~', // 正在刷新文字                refreshFailText: '刷新失败~', // 刷新失败文字                refreshSuccessText: '刷新成功~', // 刷新成功文字                pulldownText: '下拉刷新~', // 下拉中的文字                pulldownFinishText: '松开刷新~' // 下拉完成的文字            },            images, // 内置图片            elClass: '', // 组件动态class            windowInfo: {}, // 窗口信息            scrollTop: 0, // 距离顶部滚动高度            scrollViewTop: -1, // 滚动视图顶部位置            scrollViewHeight: 0, // 滚动视图高度            currentPage: 1, // 当前分页页码            currentSize: 15, // 当前分页大小            currentScrollTop: 0, // 当前滚动高度            emptyText: '暂无列表~',            loadStateText: '正在加载中~', // 加载状态文字            refreshStateText: '下拉刷新~', // 刷新状态文字            loadDisabled: false, // 是否禁用上拉加载            loading: false, // 是否加载中            refreshing: false, // 是否刷新中            refreshFinish: false, // 是否刷新完成            pulldowning: false, // 是否正在下拉            pullDownHeight: 0, // 下拉高度            showEmpty: false, // 是否显示空数据提示            showPullUp: false, // 是否显示上拉加载            showPullDown: false // 是否显示下拉刷新        };    },    methods: {        // 组件初始化        handleInit() {            // 合并配置            this.defaultOption = Object.assign(this.defaultOption, this.option);            this.showEmpty = !this.defaultOption.auto;            this.currentPage = this.defaultOption.page;            this.currentSize = this.defaultOption.size;            this.emptyText = this.defaultOption.emptyText;            this.loadStateText = this.defaultOption.loadingText;            this.refreshStateText = this.defaultOption.pulldownText;            // 计算高度            this.queryRect('.' + this.elClass).then(rect => {                // 设置组件顶部位置                this.scrollViewTop = rect.top;                // 判断是否自动加载                if (this.defaultOption.auto) this.load();            });        },        /**		 * 加载数据		 * 		 */        load() {            if (this.defaultOption.disabled || this.loading || this.loadDisabled) return;            // 开启正在加载            this.loading = true;            // 设置正在加载状态文字            this.loadStateText = this.defaultOption.loadingText;            // 显示上拉加载            this.showPullUp = true;            // 分页参数            let paging = { page: this.currentPage, size: this.currentSize };            // 触发load事件            this.$emit('load', paging);        },        // 加载成功        loadSuccess(data = {}) {            // 解构数据            const { list, total } = data;            // 判断列表是否是数组            if (Array.isArray(list)) {                // 判断列表长度                if (list.length) {                    // 判断列表长度和列表总数是否相同                    if (list.length >= total) {                        // 设置禁用上拉加载                        this.loadDisabled = true;                        // 加载状态文字                        this.loadStateText = this.defaultOption.noMoreText;                    } else {                        // 关闭禁用上拉加载                        this.loadDisabled = false;                        // 设置分页参数                        this.currentPage++;                        // 加载状态为加载中                        this.loadStateText = this.defaultOption.loadingText;                        // 加载计算                        this.loadCompute();                    }                    // 显示上拉加载                    this.showPullUp = true;                    // 隐藏空数据提示                    this.showEmpty = false;                } else {                    // 设置禁用上拉加载                    this.loadDisabled = true;                    // 隐藏上拉加载                    this.showPullUp = false;                    // 隐藏上拉加载                    this.showPullUp = false;                    // 显示空数据提示                    this.showEmpty = true;                }                // 关闭正在加载                this.loading = false;                // 触发加载成功事件                this.$emit('loadSuccess', list);            } else {                // 不是数组类型当作加载失败处理                this.loadFail();                console.error('the list must be a array');            }        },        // 加载失败        loadFail() {            // 关闭正在加载            this.loading = false;            // 关闭空数据提示            this.showEmpty = false;            // 显示上拉加载            this.showPullUp = true;            // 加载状态为加载失败            this.loadStateText = this.defaultOption.loadFailText;            // 触发加载失败事件            this.$emit('loadFail');        },        // 刷新数据        refresh() {            // 如果是下拉刷新            if (this.pullDownHeight == this.defaultOption.refresherThreshold) {                // 关闭正在加载                this.loading = false;                // 隐藏上拉加载                this.showPullUp = false;            } else {                // 开启正在加载                this.loading = true;                // 隐藏空数据提示                this.showEmpty = false;                // 显示上拉加载                this.showPullUp = true;                // 设置正在刷新状态文字                this.loadStateText = this.defaultOption.refreshingText;            }            // 设置刷新未完成            this.refreshFinish = false;            // 开启正在刷新            this.refreshing = true;            // 设置正在刷新状态文字            this.refreshStateText = this.defaultOption.refreshingText;            // 设置分页参数            this.currentPage = 1;            this.currentSize = this.defaultOption.size;            let paging = { page: this.currentPage, size: this.currentSize };            // 触发refresh事件            setTimeout(() => {                this.$emit('refresh', paging);            }, this.defaultOption.refreshDelayed);        },        // 刷新成功        refreshSuccess(data) {            // 解构数据            const { list, total } = data;            // 判断列表是否是数组            if (Array.isArray(list)) {                // 判断列表长度                if (list.length) {                    // 判断列表长度和列表总数是否相同                    if (list.length >= total) {                        // 设置禁用上拉加载                        this.loadDisabled = true;                        // 设置没有更多状态文字                        this.loadStateText = this.defaultOption.noMoreText;                    } else {                        // 设置分页参数                        this.currentPage++;                        // 关闭禁用上拉加载                        this.loadDisabled = false;                        // 设置加载中状态文字                        this.loadStateText = this.defaultOption.loadingText;                        // 开启自动加载                        this.defaultOption.auto = true;                        // 加载计算                        this.loadCompute();                    }                    // 关闭空数据提示                    this.showEmpty = false;                    // 显示上拉加载                    this.showPullUp = true;                } else {                    // 设置禁用上拉加载                    this.loadDisabled = true;                    // 隐藏上拉加载                    this.showPullUp = false;                    // 显示空数据提示                    this.showEmpty = true;                    // 设置没有更多状态文字                    this.loadStateText = this.defaultOption.noMoreText;                }                // 关闭正在加载                this.loading = false;                // 设置刷新成功状态文字                this.refreshStateText = this.defaultOption.refreshSuccessText;                // 关闭正在刷新                this.refreshing = false;                // 关闭正在下拉                this.pulldowning = false;                // 触发刷新成功事件                this.$emit('refreshSuccess', list);                setTimeout(() => {                    // 设置刷新完成                    this.refreshFinish = true;                    // 重置下拉高度                    this.pullDownHeight = 0;                    // 隐藏下拉刷新                    this.showPullDown = false;                    this.$emit('refreshSuccess');                }, this.defaultOption.refreshFinishDelayed);            } else {                // 不是数组类型当作刷新失败处理                this.refreshFail();                console.error('the list must be a array');            }        },        // 刷新失败        refreshFail() {            // 设置加载失败状态文字            this.loadStateText = this.defaultOption.refreshFailText;            // 设置刷新失败状态文字            this.refreshStateText = this.defaultOption.refreshFailText;            // 关闭正在加载            this.loading = false;            // 显示下拉加载            this.showPullUp = true;            // 关闭正在刷新            this.refreshing = false;            // 关闭正在下拉            this.pulldowning = false;            // 延迟执行刷新完成后状态            setTimeout(() => {                // 设置刷新完成                this.refreshFinish = true;                // 重置下拉高度                this.pullDownHeight = 0;                // 隐藏下拉刷新                this.showPullDown = false;                // 触发刷新失败事件                this.$emit('refreshError');            }, this.defaultOption.refreshFinishDelayed);        },        // 加载计算        loadCompute() {            // 判断是否自动加载            if (this.defaultOption.auto) {                // 延迟执行下否者可能会高度计算错误                setTimeout(() => {                    this.$nextTick(() => {                        this.queryRect('.list-content').then(rect => {                            if (rect.height <= this.scrollViewHeight) {                                this.load();                            }                        });                    });                }, 100);            }        },        /**		 * 		 * 上拉触底事件 滚动触底		 * @param {Object} e		 */         handleScrolltolower(e) {            if (this.loadDisabled) return;            this.$emit('scrolltolower', e);			//             this.load();        },        /**		 * 滚动事件		 * @param {Object} event		 * 		 */        handleScroll(event) {            this.currentScrollTop = event.detail.scrollTop;            this.$emit('scroll', event.detail);        },        /**		 * 当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。 		 * 触摸按下处理		 * @param {Object} event		 */        handleTouchStart(event) {            if (this.defaultOption.disabled) return;            this.currentTouchStartY = event.touches[0].clientY;            this.$emit('touchStart', event);        },        /**		 * 当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。		 * 触摸按下滑动处理		 * @param {Object} event		 */         handleTouchMove(event) {            if (this.defaultOption.disabled || this.currentScrollTop) return;            if (event.touches[0].clientY >= this.currentTouchStartY) {                this.pulldowning = true;                this.showPullDown = true;                let pullDownDistance = (event.touches[0].clientY - this.currentTouchStartY) * this.defaultOption.pullDownSpeed;                this.pullDownHeight = pullDownDistance > this.defaultOption.refresherThreshold ? this.defaultOption.refresherThreshold : pullDownDistance;                this.refreshStateText =                    this.pullDownHeight >= this.defaultOption.refresherThreshold ? this.defaultOption.pulldownFinishText : this.defaultOption.pulldownText;                this.$emit('touchMove', event);            }        },        /**		 * 移动端		 * 当手指从屏幕上移开时触发		 * 触摸松开处理		 * @param {Object} event		 */        handleTouchEnd(event) {            if (this.defaultOption.disabled) return;            // 当下拉高度小于下拉阈值            if (this.pullDownHeight < this.defaultOption.refresherThreshold) {                // 关闭正在下拉                this.pulldowning = false;                // 重置下拉高度                this.pullDownHeight = 0;                // 隐藏下拉刷新                this.showPullDown = false;                // 触发下拉中断事件                this.$emit('refreshStop');            } else {                this.refresh();            }            // 触发下拉触摸松开事件            this.$emit('touchEnd', event);        },        // 更新组件        updateScrollView() {            if (this.defaultOption.height) {                this.scrollViewHeight = uni.upx2px(this.defaultOption.height);            } else {                this.scrollViewHeight = this.windowInfo.windowHeight - this.scrollViewTop;            }            this.scrollViewObserve();        },        // 监听列表高度变化        listContentObserve() {            this.disconnectObserve('_listContentObserve');            const listContentObserve = this.createIntersectionObserver({                thresholds: [0, 0.5, 1]            });            listContentObserve.relativeToViewport({                // #ifdef H5                top: -(this.windowInfo.windowTop + rect.top),                // #endif                // #ifndef H5                top: -rect.top                // #endif            });        },        // 监听组件位置变化        scrollViewObserve() {            this.disconnectObserve('_scrollViewObserve');            this.$nextTick(() => {                this.queryRect('.' + this.elClass).then(rect => {                    const scrollViewObserve = this.createIntersectionObserver({                        thresholds: [0, 0.5, 1]                    });                    scrollViewObserve.relativeToViewport({                        // #ifdef H5                        top: -(this.windowInfo.windowTop + rect.top),                        // #endif                        // #ifndef H5                        top: -rect.top                        // #endif                    });                    scrollViewObserve.observe('.' + this.elClass, position => {                        // #ifdef H5                        this.scrollViewTop = position.boundingClientRect.top - this.windowInfo.windowTop;                        // #endif                        // #ifndef H5                        this.scrollViewTop = position.boundingClientRect.top;                        // #endif                    });                    this._scrollViewObserve = scrollViewObserve;                });            });        },        // 断开监听组件        disconnectObserve(observerName) {            const observer = this[observerName];            observer && observer.disconnect();        },        // 查询dom节点信息        queryRect(selector, all) {            return new Promise(resolve => {                uni.createSelectorQuery()                    .in(this)                    [all ? 'selectAll' : 'select'](selector)                    .boundingClientRect(rect => {                        if (all && Array.isArray(rect) && rect.length) {                            resolve(rect);                        }                        if (!all && rect) {                            resolve(rect);                        }                    })                    .exec();            });        },        // 16进制转RGB        hexToRgb(hex) {            const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;            hex = hex.replace(shorthandRegex, (m, r, g, b) => {                return r + r + g + g + b + b;            });            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);            return result                ? {                      r: parseInt(result[1], 16),                      g: parseInt(result[2], 16),                      b: parseInt(result[3], 16)                  }                : null;        }    },    computed: {        scrollListWrapStyle(){            let style = {};            style.background = this.defaultOption.background;            return style;        },        // 组件容器样式        listWrapStyle() {            let style = {};            const { offsetBottom } = this.defaultOption;            style.height = this.scrollViewHeight - uni.upx2px(offsetBottom) + 'px';            if (this.defaultOption.safeArea) style.paddingBottom = 'env(safe-area-inset-bottom) !important';            return style;        },        // 滚动内容样式        scrollContentStyle() {            const style = {};            const { pullDownHeight, pulldowning, showPullDown } = this;            style.transform = showPullDown ? `translateY(${pullDownHeight}px)` : `translateY(0px)`;            style.transition = pulldowning ? `transform 100ms ease-out` : `transform 200ms cubic-bezier(0.19,1.64,0.42,0.72)`;            return style;        },        // 下拉刷新样式        refreshViewStyle() {            const style = {};            const { showPullDown } = this;            style.opacity = showPullDown ? 1 : 0;            return style;        },        // 下拉中动画样式        pullDownAnimationStyle() {            const style = {};            const { refresherIconColor, refresherThreshold } = this.defaultOption;            const { refreshing, pullDownHeight } = this;            const { r, g, b } = this.hexToRgb(refresherIconColor);            const rate = pullDownHeight / refresherThreshold;            style.borderColor = `rgba(${r},${g},${b},0.2)`;            style.borderTopColor = refresherIconColor;            if (!refreshing) {                style.transform = `rotate(${360 * rate}deg)`;                style.transition = 'transform 100ms linear';            }            return style;        },        pullDownTextStyle() {            const style = {};            const { refresherTextColor } = this.defaultOption;            style.color = refresherTextColor;            return style;        },        // 上拉中动画样式        pullUpAnimationStyle() {            const style = {};            const { loadIconColor } = this.defaultOption;            const { r, g, b } = this.hexToRgb(loadIconColor);            style.borderColor = `rgba(${r},${g},${b},0.2)`;            style.borderTopColor = loadIconColor;            return style;        },        // 上拉中文字样式        pullUpTextStyle() {            const style = {};            const { loadTextColor } = this.defaultOption;            style.color = loadTextColor;            return style;        },        // 空数据提示文字样式        emptyTextStyle() {            const style = {};            const { emptyTextColor } = this.defaultOption;            style.color = emptyTextColor;            return style;        }    },    watch: {        scrollViewTop(val) {            this.updateScrollView();        }    },    created() {        this.elClass = 'scroll-view-' + this._uid;        this.windowInfo = uni.getSystemInfoSync();    },    mounted() {        this.handleInit();    }};</script><style lang="scss" scoped>.scroll-list-wrap {    box-sizing: border-box;    .scroll-view {        position: relative;        .scroll-content {            height: 100%;            display: flex;            will-change: transform;            flex-direction: column;            .pull-down-wrap {                left: 0;                width: 100%;                display: flex;                padding: 30rpx 0;                position: absolute;                align-items: flex-end;                justify-content: center;                transform: translateY(-100%);                .refresh-view {                    display: flex;                    justify-content: center;                    .pull-down-animation {                        width: 32rpx;                        height: 32rpx;                        border-width: 4rpx;                        border-style: solid;                        border-radius: 50%;                        &.refreshing {                            animation: spin 0.5s linear infinite;                        }                        @keyframes spin {                            to {                                transform: rotate(360deg);                            }                        }                    }                    .pull-down-text {                        margin-left: 10rpx;                    }                }            }            .empty-wrap-me {                top: 0;                left: 0;                width: 100%;                height: 100%;                display: flex;                position: absolute;                align-items: center;				justify-content: center;                flex-direction: column;                .empty-view-me {                    margin: auto;                    display: flex !important;                    align-items: center;                    flex-direction: column;                    .empty-image {                        width: 200rpx;                        height: 200rpx;                    }                    .empty-text {                        color: #606266;                        margin-top: 20rpx;                    }                }            }            .list-content {            }            .pull-up-wrap {                display: flex;                align-items: center;                justify-content: center;                .load-view {                    padding: 20rpx 0;                    display: flex;                    align-items: center;                    justify-content: center;                    .pull-up-animation {                        width: 32rpx;                        height: 32rpx;                        border-width: 4rpx;                        border-style: solid;                        border-radius: 50%;                        animation: spin 0.5s linear infinite;                    }                    .pull-up-text {                        margin-left: 10rpx;                    }                }            }        }    }}</style>
 |