<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>