<template>
    <view class="u-wrap" :style="{
		opacity: Number(opacity),
		borderRadius: borderRadius + 'rpx',
		// 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
		transition: `opacity ${time / 1000}s ease-in-out`
	}" :class="'u-lazy-item-' + elIndex">
        <view :class="'u-lazy-item-' + elIndex">
            <image :style="{ borderRadius: borderRadius + 'rpx', height: imgHeight }" v-if="!isError"
                class="u-lazy-item" :src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded"
                @error="loadError" @tap="clickImg">
            </image>
            <image :style="{ borderRadius: borderRadius + 'rpx', height: imgHeight }" class="u-lazy-item error" v-else
                :src="errorImg" :mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image>
        </view>
    </view>
</template>

<script>
    import {
        addUnit,
        guid,
        rpx2px
    } from '../../libs/function/index.js';
    /**
     * lazyLoad 懒加载
     * @description 懒加载使用的场景为：页面有很多图片时，APP会同时加载所有的图片，导致页面卡顿，各个位置的图片出现前后不一致等.
     * @tutorial https://uview-plus.jiangruyi.com/components/lazy-load.html
     * @property {String Number} index 用户自定义值，在事件触发时回调，用以区分是哪个图片
     * @property {String} image 图片路径
     * @property {String} loading-img 预加载时的占位图
     * @property {String} error-img 图片加载出错时的占位图
     * @property {String} threshold 触发加载时的位置，见上方说明，单位 rpx（默认300）
     * @property {String Number} duration 图片加载成功时，淡入淡出时间，单位ms（默认）
     * @property {String} effect 图片加载成功时，淡入淡出的css动画效果（默认ease-in-out）
     * @property {Boolean} is-effect 图片加载成功时，是否启用淡入淡出效果（默认true）
     * @property {String Number} border-radius 图片圆角值，单位rpx（默认0）
     * @property {String Number} height 图片高度，注意：实际高度可能受img-mode参数影响（默认450）
     * @property {String Number} mg-mode 图片的裁剪模式，详见image组件裁剪模式（默认widthFix）
     * @event {Function} click 点击图片时触发
     * @event {Function} load 图片加载成功时触发
     * @event {Function} error 图片加载失败时触发
     * @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
     */
    export default {
        name: 'u-lazy-load',
        props: {
            index: {
                type: [Number, String]
            },
            // 要显示的图片
            image: {
                type: String,
                default: ''
            },
            // 图片裁剪模式
            imgMode: {
                type: String,
                default: 'widthFix'
            },
            // 占位图片路径
            loadingImg: {
                type: String,
                default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
            },
            // 加载失败的错误占位图
            errorImg: {
                type: String,
                default: 'data:image/png;base64, '
            },
            // 图片进入可见区域前多少像素时，单位rpx，开始加载图片
            // 负数为图片超出屏幕底部多少距离后触发懒加载，正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上)
            threshold: {
                type: [Number, String],
                default: 100
            },
            // 淡入淡出动画的过渡时间
            duration: {
                type: [Number, String],
                default: 500
            },
            // 渡效果的速度曲线，各个之间差别不大，因为这是淡入淡出，且时间很短，不是那些变形或者移动的情况，会明显
            // linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
            effect: {
                type: String,
                default: 'ease-in-out'
            },
            // 是否使用过渡效果
            isEffect: {
                type: Boolean,
                default: true
            },
            // 圆角值
            borderRadius: {
                type: [Number, String],
                default: 0
            },
            // 图片高度，单位rpx
            height: {
                type: [Number, String],
                default: '200'
            }
        },
        data() {
            return {
                isShow: false,
                opacity: 1,
                time: this.duration,
                loadStatus: '', // 默认是懒加载中的状态
                isError: false, // 图片加载失败
                elIndex: guid()
            }
        },
        computed: {
            // 将threshold从rpx转为px
            getThreshold() {
                // 先取绝对值，因为threshold可能是负数，最后根据this.threshold是正数或者负数，重新还原
                let thresholdPx = rpx2px(Math.abs(this.threshold));
                return this.threshold < 0 ? -thresholdPx : thresholdPx;
            },
            // 计算图片的高度，可能为auto，带%，或者直接数值
            imgHeight() {
                return addUnit(this.height);
            }
        },
        created() {
            // 由于一些特殊原因，不能将此变量放到data中定义
            this.observer = {};
        },
        watch: {
            isShow(nVal) {
                // 如果是不开启过渡效果，直接返回
                if (!this.isEffect) return;
                this.time = 0;
                // 原来opacity为1(不透明，是为了显示占位图)，改成0(透明，意味着该元素显示的是背景颜色，默认的白色)，再改成1，是为了获得过渡效果
                this.opacity = 0;
                // 延时30ms，否则在浏览器H5，过渡效果无效
                setTimeout(() => {
                    this.time = this.duration;
                    this.opacity = 1;
                }, 30)
            },
            // 图片路径发生变化时，需要重新标记一些变量，否则会一直卡在某一个状态，比如isError
            image(n) {
                if (!n) {
                    // 如果传入null或者''，或者undefined，标记为错误状态
                    this.isError = true;
                } else {
                    this.init();
                    this.isError = false;
                }
            }
        },
        emits: ['click', 'load', 'error'],
        methods: {
            // 用于重新初始化
            init() {
                this.isError = false;
                this.loadStatus = '';
            },
            // 点击图片触发的事件,loadlazy-还是懒加载中状态，loading-图片正在加载，loaded-图片加加载完成
            clickImg() {
                let whichImg = '';
                // 如果isShow为false，意味着图片还没开始加载，点击的只能是最开始的占位图
                if (this.isShow == false) whichImg = 'lazyImg';
                // 如果isError为true，意味着图片加载失败，这是只剩下错误的占位图，所以点击的只能是错误占位图
                // 当然，也可以给错误的占位图元素绑定点击事件，看你喜欢~
                else if (this.isError == true) whichImg = 'errorImg';
                // 总共三张图片，除了两个占位图，剩下的只能是正常的那张图片了
                else whichImg = 'realImg';
                // 只通知当前图片的index
                this.$emit('click', this.index);
            },
            // 图片加载完成事件，可能是加载占位图时触发，也可能是加载真正的图片完成时触发，通过isShow区分
            imgLoaded() {
                // 占位图加载完成
                if (this.loadStatus == '') {
                    this.loadStatus = 'lazyed';
                }
                // 真正的图片加载完成 
                else if (this.loadStatus == 'lazyed') {
                    this.loadStatus = 'loaded';
                    this.$emit('load', this.index);
                }
            },
            // 错误的图片加载完成
            errorImgLoaded() {
                this.$emit('error', this.index);
            },
            // 图片加载失败
            loadError() {
                this.isError = true;
            },
            disconnectObserver(observerName) {
                const observer = this[observerName];
                observer && observer.disconnect();
            },
        },
        beforeUnmount() {
            // 销毁页面时，可能还没触发某张很底部的懒加载图片，所以把这个事件给去掉
            //observer.disconnect();
        },
        mounted() {
            // 此uOnReachBottom事件由mixin.js发出，目的是让页面到底时，保证所有图片都进行加载，做到绝对稳定且可靠
            this.$nextTick(() => {
                uni.$once('uOnReachBottom', () => {
                    if (!this.isShow) this.isShow = true;
                });
            })
            // mounted的时候，不一定挂载了这个元素，延时30ms，否则会报错或者不报错，但是也没有效果
           
            setTimeout(() => {
                // #ifndef APP-NVUE
                // 这里是组件内获取布局状态，不能用uni.createIntersectionObserver，而必须用this.createIntersectionObserver
                // this.disconnectObserver('contentObserver');
                // nvue 里不支持
                const contentObserver = uni.createIntersectionObserver(this);
                // 要理解这里怎么计算的，请看这个：
                // https://blog.csdn.net/qq_25324335/article/details/83687695
                contentObserver.relativeToViewport({
                    bottom: this.getThreshold,
                }).observe('.u-lazy-item-' + this.elIndex, (res) => {
                    // console.log('relativeToViewport', res)
                    if (res.intersectionRatio > 0) {
                        // 懒加载状态改变
                        this.isShow = true;
                        // 图片为空时显示错误
                        if (!this.image) {
                            this.loadError();
                        }
                        // 如果图片已经加载，去掉监听，减少性能的消耗
                        this.disconnectObserver('contentObserver');
                    }
                })
                this.contentObserver = contentObserver;
                // #endif
                // #ifdef APP-NVUE
                this.isShow = true;
                // #endif
            }, 30)
        }
    }
</script>

<style scoped lang="scss">
    .u-wrap {
        background-color: #eee;
        overflow: hidden;
    }

    .u-lazy-item {
        // 骗系统开启硬件加速
        transform: transition3d(0, 0, 0);
        /* #ifndef APP-NVUE */
        // 防止图片加载“闪一下”
        will-change: transform;
        display: block;
        width: 100%;
        /* #endif */
    }
</style>