曹睿
2025-04-21 1e5646aadae902d9f9043cc0d79395bf6b06a38c
feat: 完成框架
已修改14个文件
已添加38个文件
7721 ■■■■■ 文件已修改
.env.development 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/auth/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/card-title/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/scan/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging-cell/z-paging-cell.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging-empty-view/z-paging-empty-view.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging-swiper-item/z-paging-swiper-item.vue 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging-swiper/z-paging-swiper.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/components/z-paging-load-more.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/components/z-paging-refresh.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/config/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/css/z-paging-main.css 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/css/z-paging-static.css 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/i18n/en.json 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/i18n/index.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/i18n/zh-Hans.json 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/i18n/zh-Hant.json 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/hooks/useZPaging.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/hooks/useZPagingComp.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/back-to-top.js 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/chat-record-mode.js 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/common-layout.js 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/data-handle.js 736 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/empty.js 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/i18n.js 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/load-more.js 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/loading.js 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/nvue.js 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/refresher.js 831 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/scroller.js 550 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/modules/virtual-list.js 555 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-constant.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-enum.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-interceptor.js 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-main.js 515 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-mixin.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-static.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/js/z-paging-utils.js 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/wxs/z-paging-renderjs.js 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/wxs/z-paging-wxs.wxs 382 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/z-paging/z-paging.vue 538 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useZebraScan.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/detail/twistDetail.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/detail/wireDetail.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/monofil.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/report/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/wire/backman/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/wire/receive/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/wire/report/wire.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/wire/selfInspect/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -4,7 +4,7 @@
VITE_APP_PORT = 4096
# API åŸºç¡€è·¯å¾„,开发环境下的请求前缀
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_API = '/mes'
# API æœåŠ¡å™¨çš„ URL
VITE_APP_API_URL = https://api.youlai.tech
VITE_APP_API_URL = 'http://114.132.189.42:7002'
src/api/auth/index.ts
@@ -11,7 +11,7 @@
  login(data: LoginFormData): Promise<LoginResult> {
    console.log("data", data);
    return request<LoginResult>({
      url: "/api/v1/auth/login",
      url: "/login",
      method: "POST",
      data: data,
      header: {
src/components/card-title/index.vue
@@ -1,6 +1,6 @@
<template>
  <view class="flex items-center justify-between page">
    <view class="ml-3">
  <view :class="['flex', 'items-center', 'justify-between', 'page', full ? 'mx-3' : '']">
    <view>
      <text class="title">{{ title }}</text>
    </view>
    <view v-if="hideAction" class="flex justify-center">
@@ -13,6 +13,10 @@
</template>
<script setup lang="ts">
defineProps({
  full: {
    type: Boolean,
    default: true,
  },
  title: String,
  hideAction: Boolean,
});
@@ -26,7 +30,6 @@
<style lang="scss" scoped>
.page {
  padding: 10px 0;
  margin: 0 10px;
  .title {
    position: relative;
    margin-left: 10px;
src/components/scan/index.vue
@@ -47,7 +47,8 @@
          //斑马 TC20
          var banMaSacanInfo = intent.getStringExtra(
            "com.motorolasolutions.emdk.datawedge.data_string"
          ); // callback(intent.getStringExtra('com.motorolasolutions.emdk.datawedge.data_string'));
          );
          // callback(intent.getStringExtra('com.motorolasolutions.emdk.datawedge.data_string'));
          console.log("斑马扫描结果", banMaSacanInfo);
          // ä¼ å…¥æŽ¥æ”¶åˆ°çš„参数
          that.queryCode(banMaSacanInfo);
@@ -84,6 +85,18 @@
      });
      // #endif
    },
    triggerScan() {
      console.log("触发扫描");
      // èŽ·å–Android意图类
      let Intent = plus.android.importClass("android.content.Intent");
      // å®žä¾‹åŒ–意图
      let intent = new Intent();
      // å®šä¹‰æ„å›¾ï¼Œç”±åŽ‚å•†æä¾›(此处设置为东大的: å¼€å§‹æ‰«æå¹¿æ’­com.scan.onStartScan,对应的停止扫描广播为com.scan.onEndScan)
      intent.setAction("com.symbol.datawedge.api.ACTION");
      intent.putExtra("com.symbol.datawedge.api.SOFT_SCAN_TRIGGER", "START_SCANNING");
      // å¹¿æ’­è¿™ä¸ªæ„å›¾
      main.sendBroadcast(intent);
    },
  },
};
</script>
src/components/z-paging-cell/z-paging-cell.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<!-- z-paging -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
<!-- åé¦ˆQQ群:790460711 -->
<!-- z-paging-cell,用于在nvue中使用cell包裹,vue中使用view包裹 -->
<template>
    <!-- #ifdef APP-NVUE -->
    <cell :style="[cellStyle]" @touchstart="onTouchstart">
        <slot />
    </cell>
    <!-- #endif -->
    <!-- #ifndef APP-NVUE -->
    <view :style="[cellStyle]" @touchstart="onTouchstart">
        <slot />
    </view>
    <!-- #endif -->
</template>
<script>
    /**
     * z-paging-cell ç»„ä»¶
     * @description ç”¨äºŽå…¼å®¹ nvue å’Œ vue ä¸­çš„ cell æ¸²æŸ“。因为在 nvue ä¸­ z-paging å†…置的是 list,因此列表 item å¿…须使用 cell åŒ…住;在 vue ä¸­ä¸èƒ½ä½¿ç”¨ cell,否则会报组件找不到的错误。此子组件为了兼容这两种情况,内部作了条件编译处理。
     * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-cell配置
     * @notice ä»¥ä¸‹ä¸º z-paging-cell çš„配置项
     * @property {Object} cellStyle cell æ ·å¼ï¼Œé»˜è®¤ä¸º {}
     * @example <z-paging-cell :cellStyle="{ backgroundColor: '#f0f0f0' }"></z-paging-cell>
     */
    export default {
        name: "z-paging-cell",
        props: {
            //cellStyle
            cellStyle: {
                type: Object,
                default: function() {
                    return {}
                }
            }
        },
        methods: {
            onTouchstart(e) {
                this.$emit('touchstart', e);
            }
        }
    }
</script>
src/components/z-paging-empty-view/z-paging-empty-view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<!-- z-paging -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
<!-- åé¦ˆQQ群:790460711 -->
<!-- ç©ºæ•°æ®å ä½view,此组件支持easycom规范,可以在项目中直接引用 -->
<template>
    <view :class="{'zp-container':true,'zp-container-fixed':emptyViewFixed}" :style="[finalEmptyViewStyle]" @click="emptyViewClick">
        <view class="zp-main">
            <image v-if="!emptyViewImg.length" :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" :style="[emptyViewImgStyle]" :src="emptyImg" />
            <image v-else :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg" />
            <text class="zp-main-title" :class="{'zp-main-title-rpx':unit==='rpx','zp-main-title-px':unit==='px'}" :style="[emptyViewTitleStyle]">{{emptyViewText}}</text>
            <text v-if="showEmptyViewReload" :class="{'zp-main-error-btn':true,'zp-main-error-btn-rpx':unit==='rpx','zp-main-error-btn-px':unit==='px'}" :style="[emptyViewReloadStyle]" @click.stop="reloadClick">{{emptyViewReloadText}}</text>
        </view>
    </view>
</template>
<script>
    import zStatic from '../z-paging/js/z-paging-static'
    /**
     * z-paging-empty-view ç©ºæ•°æ®ç»„ä»¶
     * @description é€šç”¨çš„ z-paging ç©ºæ•°æ®ç»„ä»¶
     * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-empty-view配置
     * @property {Boolean} emptyViewFixed ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦é“ºæ»¡ z-paging,默认为 false。若设置为 true,则为填充满 z-paging çš„剩余部分
     * @property {String} emptyViewText ç©ºæ•°æ®å›¾æè¿°æ–‡å­—,默认为 '没有数据哦~'
     * @property {String} emptyViewImg ç©ºæ•°æ®å›¾å›¾ç‰‡ï¼Œé»˜è®¤ä½¿ç”¨ z-paging å†…置的图片 (建议使用绝对路径,开头不要添加 "@",请以 "/" å¼€å¤´)
     * @property {String} emptyViewReloadText ç©ºæ•°æ®å›¾ç‚¹å‡»é‡æ–°åŠ è½½æ–‡å­—ï¼Œé»˜è®¤ä¸º '重新加载'
     * @property {Object} emptyViewStyle ç©ºæ•°æ®å›¾æ ·å¼ï¼Œå¯è®¾ç½®ç©ºæ•°æ® view çš„ top ç­‰ï¼Œå¦‚: empty-view-style="{'top':'100rpx'}" (如果空数据图不是 fixed å¸ƒå±€ï¼Œåˆ™æ­¤å¤„是 margin-top),默认为 {}
     * @property {Object} emptyViewImgStyle ç©ºæ•°æ®å›¾ img æ ·å¼ï¼Œé»˜è®¤ä¸º {}
     * @property {Object} emptyViewTitleStyle ç©ºæ•°æ®å›¾æè¿°æ–‡å­—样式,默认为 {}
     * @property {Object} emptyViewReloadStyle ç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®æ ·å¼ï¼Œé»˜è®¤ä¸º {}
     * @property {Boolean} showEmptyViewReload æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®(无数据时),默认为 false
     * @property {Boolean} isLoadFailed æ˜¯å¦æ˜¯åŠ è½½å¤±è´¥ï¼Œé»˜è®¤ä¸º false
     * @property {String} unit ç©ºæ•°æ®å›¾ä¸­å¸ƒå±€çš„单位,默认为 'rpx'
     * @event {Function} reload ç‚¹å‡»äº†é‡æ–°åŠ è½½æŒ‰é’®
     * @event {Function} viewClick ç‚¹å‡»äº†ç©ºæ•°æ®å›¾ view
     * @example <z-paging-empty-view empty-view-text="暂无数据" />
     */
    export default {
        name: "z-paging-empty-view",
        data() {
            return {
            };
        },
        props: {
            // ç©ºæ•°æ®æè¿°æ–‡å­—
            emptyViewText: {
                type: String,
                default: '没有数据哦~'
            },
            // ç©ºæ•°æ®å›¾ç‰‡
            emptyViewImg: {
                type: String,
                default: ''
            },
            // æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®
            showEmptyViewReload: {
                type: Boolean,
                default: false
            },
            // ç©ºæ•°æ®ç‚¹å‡»é‡æ–°åŠ è½½æ–‡å­—
            emptyViewReloadText: {
                type: String,
                default: '重新加载'
            },
            // æ˜¯å¦æ˜¯åŠ è½½å¤±è´¥
            isLoadFailed: {
                type: Boolean,
                default: false
            },
            // ç©ºæ•°æ®å›¾æ ·å¼
            emptyViewStyle: {
                type: Object,
                default: function() {
                    return {}
                }
            },
            // ç©ºæ•°æ®å›¾img样式
            emptyViewImgStyle: {
                type: Object,
                default: function() {
                    return {}
                }
            },
            // ç©ºæ•°æ®å›¾æè¿°æ–‡å­—样式
            emptyViewTitleStyle: {
                type: Object,
                default: function() {
                    return {}
                }
            },
            // ç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®æ ·å¼
            emptyViewReloadStyle: {
                type: Object,
                default: function() {
                    return {}
                }
            },
            // ç©ºæ•°æ®å›¾z-index
            emptyViewZIndex: {
                type: Number,
                default: 9
            },
            // ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦ä½¿ç”¨fixed布局并铺满z-paging
            emptyViewFixed: {
                type: Boolean,
                default: true
            },
            // ç©ºæ•°æ®å›¾ä¸­å¸ƒå±€çš„单位,默认为rpx
            unit: {
                type: String,
                default: 'rpx'
            }
        },
        computed: {
            emptyImg() {
                return this.isLoadFailed ? zStatic.base64Error : zStatic.base64Empty;
            },
            finalEmptyViewStyle(){
                this.emptyViewStyle['z-index'] = this.emptyViewZIndex;
                return this.emptyViewStyle;
            }
        },
        methods: {
            // ç‚¹å‡»äº†reload按钮
            reloadClick() {
                this.$emit('reload');
            },
            // ç‚¹å‡»äº†ç©ºæ•°æ®view
            emptyViewClick() {
                this.$emit('viewClick');
            }
        }
    }
</script>
<style scoped>
    .zp-container{
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        align-items: center;
        justify-content: center;
    }
    .zp-container-fixed {
        /* #ifndef APP-NVUE */
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        /* #endif */
        /* #ifdef APP-NVUE */
        flex: 1;
        /* #endif */
    }
    .zp-main{
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        flex-direction: column;
        align-items: center;
        padding: 50rpx 0rpx;
    }
    .zp-main-image-rpx {
        width: 240rpx;
        height: 240rpx;
    }
    .zp-main-image-px {
        width: 120px;
        height: 120px;
    }
    .zp-main-title {
        color: #aaaaaa;
        text-align: center;
    }
    .zp-main-title-rpx {
        font-size: 28rpx;
        margin-top: 10rpx;
        padding: 0rpx 20rpx;
    }
    .zp-main-title-px {
        font-size: 14px;
        margin-top: 5px;
        padding: 0px 10px;
    }
    .zp-main-error-btn {
        border: solid 1px #dddddd;
        color: #aaaaaa;
    }
    .zp-main-error-btn-rpx {
        font-size: 28rpx;
        padding: 8rpx 24rpx;
        border-radius: 6rpx;
        margin-top: 50rpx;
    }
    .zp-main-error-btn-px {
        font-size: 14px;
        padding: 4px 12px;
        border-radius: 3px;
        margin-top: 25px;
    }
</style>
src/components/z-paging-swiper-item/z-paging-swiper-item.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
<!-- z-paging -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
<!-- åé¦ˆQQ群:790460711 -->
<!-- æ»‘动切换选项卡swiper-item,此组件支持easycom规范,可以在项目中直接引用 -->
<template>
    <view class="zp-swiper-item-container">
        <z-paging ref="paging" :fixed="false"
            :auto="false" :useVirtualList="useVirtualList" :useInnerList="useInnerList" :cellKeyName="cellKeyName" :innerListStyle="innerListStyle"
            :preloadPage="preloadPage" :cellHeightMode="cellHeightMode" :virtualScrollFps="virtualScrollFps" :virtualListCol="virtualListCol"
            @query="_queryList" @listChange="_updateList">
            <slot />
            <template #header>
                <slot name="header"/>
            </template>
            <template #cell="{item,index}">
                <slot name="cell" :item="item" :index="index"/>
            </template>
            <template #footer>
                <slot name="footer"/>
            </template>
        </z-paging>
    </view>
</template>
<script>
    import zPaging from '../z-paging/z-paging'
    /**
     * z-paging-swiper-item ç»„ä»¶
     * @description swiper+list极简写法中使用到,实际上就是对普通的swiper+list中swiper-item的包装封装,用以简化写法,但个性化配置局限较多
     * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper-item配置
     * @notice ä»¥ä¸‹ä¸º z-paging-swiper-item çš„配置项
     * @property {Number} tabIndex å½“前组件的 index,也就是当前组件是 swiper ä¸­çš„第几个,默认为 0
     * @property {Number} currentIndex å½“前 swiper åˆ‡æ¢åˆ°ç¬¬å‡ ä¸ª index,默认为 0
     * @property {Boolean} useVirtualList æ˜¯å¦ä½¿ç”¨è™šæ‹Ÿåˆ—表,默认为 false
     * @property {Boolean} useInnerList æ˜¯å¦åœ¨ z-paging å†…部循环渲染列表(内置列表),默认为 false。若 useVirtualList ä¸º true,则此项恒为 true
     * @property {String} cellKeyName å†…置列表 cell çš„ key åç§°ï¼Œä»… nvue æœ‰æ•ˆï¼Œåœ¨ nvue ä¸­å¼€å¯ useInnerList æ—¶å¿…须填此项,默认为 ''
     * @property {Object} innerListStyle innerList æ ·å¼ï¼Œé»˜è®¤ä¸º {}
     * @property {Number|String} preloadPage é¢„加载的列表可视范围(列表高度)页数,默认为 12。此数值越大,则虚拟列表中加载的 dom è¶Šå¤šï¼Œå†…存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
     * @property {String} cellHeightMode è™šæ‹Ÿåˆ—表 cell é«˜åº¦æ¨¡å¼ï¼Œé»˜è®¤ä¸º 'fixed',也就是每个 cell é«˜åº¦å®Œå…¨ç›¸åŒï¼Œå°†ä»¥ç¬¬ä¸€ä¸ª cell é«˜åº¦ä¸ºå‡†è¿›è¡Œè®¡ç®—。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】
     * @property {Number|String} virtualListCol è™šæ‹Ÿåˆ—表列数,默认为 1。常用于每行有多列的情况,例如每行有 2 åˆ—数据,需要将此值设置为 2
     * @property {Number|String} virtualScrollFps è™šæ‹Ÿåˆ—表 scroll å–样帧率,默认为 60,过高可能出现卡顿等问题
     * @example <z-paging-swiper-item ref="swiperItem" :tabIndex="index" :currentIndex="current" @query="queryList" @updateList="updateList"></z-paging-swiper-item>
     */
    export default {
        name: "z-paging-swiper-item",
        components: { zPaging },
        data() {
            return {
                firstLoaded: false
            }
        },
        props: {
            // å½“前组件的index,也就是当前组件是swiper中的第几个
            tabIndex: {
                type: Number,
                default: function() {
                    return 0
                }
            },
            // å½“前swiper切换到第几个index
            currentIndex: {
                type: Number,
                default: function() {
                    return 0
                }
            },
            // æ˜¯å¦ä½¿ç”¨è™šæ‹Ÿåˆ—表,默认为否
            useVirtualList: {
                type: Boolean,
                default: false
            },
            // æ˜¯å¦åœ¨z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
            useInnerList: {
                type: Boolean,
                default: false
            },
            // å†…置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
            cellKeyName: {
                type: String,
                default: ''
            },
            // innerList样式
            innerListStyle: {
                type: Object,
                default: function() {
                    return {};
                }
            },
            // é¢„加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
            preloadPage: {
                type: [Number, String],
                default: 12
            },
            // è™šæ‹Ÿåˆ—表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
            cellHeightMode: {
                type: String,
                default: 'fixed'
            },
            // è™šæ‹Ÿåˆ—表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
            virtualListCol: {
                type: [Number, String],
                default: 1
            },
            // è™šæ‹Ÿåˆ—表scroll取样帧率,默认为60,过高可能出现卡顿等问题
            virtualScrollFps: {
                type: [Number, String],
                default: 60
            },
        },
        watch: {
            currentIndex: {
                handler(newVal, oldVal) {
                    if (newVal === this.tabIndex) {
                        // æ‡’加载,当滑动到当前的item时,才去加载
                        if (!this.firstLoaded) {
                            this.$nextTick(()=>{
                                let delay = 5;
                                // #ifdef MP-TOUTIAO
                                delay = 100;
                                // #endif
                                setTimeout(() => {
                                    this.$refs.paging.reload().catch(() => {});
                                }, delay);
                            })
                        }
                    }
                },
                immediate: true
            }
        },
        methods: {
            reload(data) {
                return this.$refs.paging.reload(data);
            },
            complete(data) {
                this.firstLoaded = true;
                return this.$refs.paging.complete(data);
            },
            _queryList(pageNo, pageSize, from) {
                this.$emit('query', pageNo, pageSize, from);
            },
            _updateList(list) {
                this.$emit('updateList', list);
            }
        }
    }
</script>
<style scoped>
    .zp-swiper-item-container {
        /* #ifndef APP-NVUE */
        height: 100%;
        /* #endif */
        /* #ifdef APP-NVUE */
        flex: 1;
        /* #endif */
    }
</style>
src/components/z-paging-swiper/z-paging-swiper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
<!-- z-paging -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
<!-- åé¦ˆQQ群:790460711 -->
<!-- æ»‘动切换选项卡swiper容器,此组件支持easycom规范,可以在项目中直接引用 -->
<template>
    <view :class="fixed?'zp-swiper-container zp-swiper-container-fixed':'zp-swiper-container'" :style="[finalSwiperStyle]">
        <!-- #ifndef APP-PLUS -->
        <view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
        <!-- #endif -->
        <slot v-if="zSlots.top" name="top" />
        <view class="zp-swiper-super">
            <view v-if="zSlots.left" :class="{'zp-swiper-left':true,'zp-absoulte':isOldWebView}">
                <slot name="left" />
            </view>
            <view :class="{'zp-swiper':true,'zp-absoulte':isOldWebView}" :style="[swiperContentStyle]">
                <slot />
            </view>
            <view v-if="zSlots.right" :class="{'zp-swiper-right':true,'zp-absoulte zp-right':isOldWebView}">
                <slot name="right" />
            </view>
        </view>
        <slot v-if="zSlots.bottom" name="bottom" />
    </view>
</template>
<script>
    import commonLayoutModule from '../z-paging/js/modules/common-layout'
    /**
     * z-paging-swiper ç»„ä»¶
     * @description åœ¨ swiper ä¸­ä½¿ç”¨ z-paging æ—¶ï¼ˆå·¦å³æ»‘动切换列表),在根节点使用 z-paging-swiper,其相当于一个 view å®¹å™¨ï¼Œé»˜è®¤é“ºæ»¡å…¨å±ï¼Œå¯å…è®¡ç®—高度直接插入 swiper çš„视图容器。
     * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper配置
     * @property {Boolean} fixed æ˜¯å¦ä½¿ç”¨ fixed å¸ƒå±€ï¼Œé»˜è®¤ä¸º true
     * @property {Boolean} safeAreaInsetBottom æ˜¯å¦å¼€å¯åº•部安全区域适配,默认为 false
     * @property {Object} swiperStyle z-paging-swiper æ ·å¼ï¼Œé»˜è®¤ä¸º {}
     * @example <z-paging-swiper :safeAreaInsetBottom="true"></z-paging-swiper>
     */
    export default {
        name: "z-paging-swiper",
        mixins: [commonLayoutModule],
        data() {
            return {
                swiperContentStyle: {}
            };
        },
        props: {
            // æ˜¯å¦ä½¿ç”¨fixed布局,默认为是
            fixed: {
                type: Boolean,
                default: true
            },
            // æ˜¯å¦å¼€å¯åº•部安全区域适配
            safeAreaInsetBottom: {
                type: Boolean,
                default: false
            },
            // z-paging-swiper样式
            swiperStyle: {
                type: Object,
                default: function() {
                    return {};
                },
            }
        },
        mounted() {
            this.$nextTick(() => {
                this.systemInfo = this._getSystemInfoSync();
                setTimeout(this.updateFixedLayout, 100)
            })
            // #ifndef APP-PLUS
            this._getCssSafeAreaInsetBottom();
            // #endif
            this.updateLeftAndRightWidth();
            this.swiperContentStyle = { 'flex': '1' };
            // #ifndef APP-NVUE
            this.swiperContentStyle = { width: '100%',height: '100%' };
            // #endif
        },
        computed: {
            finalSwiperStyle() {
                const swiperStyle = { ...this.swiperStyle };
                if (!this.systemInfo) return swiperStyle;
                const windowTop = this.windowTop;
                const windowBottom = this.systemInfo.windowBottom;
                if (this.fixed) {
                    if (windowTop && !swiperStyle.top) {
                        swiperStyle.top = windowTop + 'px';
                    }
                    if (!swiperStyle.bottom) {
                        let bottom = windowBottom || 0;
                        bottom += this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
                        if (bottom > 0) {
                            swiperStyle.bottom = bottom + 'px';
                        }
                    }
                }
                return swiperStyle;
            }
        },
        methods: {
            // æ›´æ–°slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
            updateLeftAndRightWidth() {
                if (!this.isOldWebView) return;
                this.$nextTick(() => this._updateLeftAndRightWidth(this.swiperContentStyle, 'zp-swiper'));
            }
        }
    }
</script>
<style scoped>
    .zp-swiper-container {
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        flex-direction: column;
        flex: 1;
    }
    .zp-swiper-container-fixed {
        position: fixed;
        /* #ifndef APP-NVUE */
        height: auto;
        width: auto;
        /* #endif */
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
    }
    .zp-safe-area-inset-bottom {
        position: absolute;
        /* #ifndef APP-PLUS */
        height: env(safe-area-inset-bottom);
        /* #endif */
    }
    .zp-swiper-super {
        flex: 1;
        overflow: hidden;
        position: relative;
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        flex-direction: row;
    }
    .zp-swiper-left,.zp-swiper-right{
        /* #ifndef APP-NVUE */
        height: 100%;
        /* #endif */
    }
    .zp-swiper {
        flex: 1;
        /* #ifndef APP-NVUE */
        height: 100%;
        width: 100%;
        /* #endif */
    }
    .zp-absoulte {
        /* #ifndef APP-NVUE */
        position: absolute;
        top: 0;
        width: auto;
        /* #endif */
    }
    .zp-swiper-item {
        height: 100%;
    }
</style>
src/components/z-paging/components/z-paging-load-more.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
<!-- [z-paging]上拉加载更多view -->
<template>
    <view class="zp-l-container" :class="{'zp-l-container-rpx':c.unit==='rpx','zp-l-container-px':c.unit==='px'}" :style="[c.customStyle]" @click="doClick">
        <template v-if="!c.hideContent">
            <!-- åº•部加载更多没有更多数据分割线 -->
            <text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
            <!-- åº•部加载更多loading -->
            <!-- #ifndef APP-NVUE -->
            <image v-if="finalStatus===M.Loading&&!!c.loadingIconCustomImage"
                :src="c.loadingIconCustomImage" :style="[c.iconCustomStyle]" :class="{'zp-l-line-loading-custom-image':true,'zp-l-line-loading-custom-image-animated':c.loadingAnimated,'zp-l-line-loading-custom-image-rpx':c.unit==='rpx','zp-l-line-loading-custom-image-px':c.unit==='px'}" />
            <image v-if="finalStatus===M.Loading&&finalLoadingIconType==='flower'&&!c.loadingIconCustomImage.length"
                :class="{'zp-line-loading-image':true,'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[c.iconCustomStyle]" :src="zTheme.flower[ts]" />
            <!-- #endif -->
            <!-- #ifdef APP-NVUE -->
            <!-- åœ¨nvue中底部加载更多loading使用系统自带的 -->
            <view>
                <loading-indicator v-if="finalStatus===M.Loading&&finalLoadingIconType!=='circle'" :class="{'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[{color:zTheme.indicator[ts]}]" :animating="true" />
            </view>
            <!-- #endif -->
            <!-- åº•部加载更多文字 -->
            <text v-if="finalStatus===M.Loading&&finalLoadingIconType==='circle'&&!c.loadingIconCustomImage.length"
                class="zp-l-circle-loading-view" :class="{'zp-l-circle-loading-view-rpx':c.unit==='rpx','zp-l-circle-loading-view-px':c.unit==='px'}" :style="[{borderColor:zTheme.circleBorder[ts],borderTopColor:zTheme.circleBorderTop[ts]},c.iconCustomStyle]" />
            <text v-if="!c.isChat||(!c.chatDefaultAsLoading&&finalStatus===M.Default)||finalStatus===M.Fail" :class="{'zp-l-text-rpx':c.unit==='rpx','zp-l-text-px':c.unit==='px'}" :style="[{color:zTheme.title[ts]},c.titleCustomStyle]">{{ownLoadingMoreText}}</text>
            <!-- åº•部加载更多没有更多数据分割线 -->
            <text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
        </template>
    </view>
</template>
<script>
    import zStatic from '../js/z-paging-static'
    import Enum from '../js/z-paging-enum'
    export default {
        name: 'z-paging-load-more',
        data() {
            return {
                M: Enum.More,
                zTheme: {
                    title: { white: '#efefef', black: '#a4a4a4' },
                    line: { white: '#efefef', black: '#eeeeee' },
                    circleBorder: { white: '#aaaaaa', black: '#c8c8c8' },
                    circleBorderTop: { white: '#ffffff', black: '#444444' },
                    flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
                    indicator: { white: '#eeeeee', black: '#777777' }
                }
            };
        },
        props: ['zConfig'],
        computed: {
            ts() {
                return this.c.defaultThemeStyle;
            },
            // åº•部加载更多配置
            c() {
                return this.zConfig || {};
            },
            // åº•部加载更多文字
            ownLoadingMoreText() {
                return {
                    [this.M.Default]: this.c.defaultText,
                    [this.M.Loading]: this.c.loadingText,
                    [this.M.NoMore]: this.c.noMoreText,
                    [this.M.Fail]: this.c.failText,
                }[this.finalStatus];
            },
            // åº•部加载更多状态
            finalStatus() {
                if (this.c.defaultAsLoading && this.c.status === this.M.Default) return this.M.Loading;
                return this.c.status;
            },
            // åŠ è½½æ›´å¤šicon类型
            finalLoadingIconType() {
                // #ifdef APP-NVUE
                return 'flower';
                // #endif
                return this.c.loadingIconType;
            }
        },
        methods: {
            // ç‚¹å‡»äº†åŠ è½½æ›´å¤š
            doClick() {
                this.$emit('doClick');
            }
        }
    }
</script>
<style scoped>
    @import "../css/z-paging-static.css";
    .zp-l-container {
        /* #ifndef APP-NVUE */
        clear: both;
        display: flex;
        /* #endif */
        flex-direction: row;
        align-items: center;
        justify-content: center;
    }
    .zp-l-container-rpx {
        height: 80rpx;
        font-size: 27rpx;
    }
    .zp-l-container-px {
        height: 40px;
        font-size: 14px;
    }
    .zp-l-line-loading-custom-image {
        color: #a4a4a4;
    }
    .zp-l-line-loading-custom-image-rpx {
        margin-right: 8rpx;
        width: 28rpx;
        height: 28rpx;
    }
    .zp-l-line-loading-custom-image-px {
        margin-right: 4px;
        width: 14px;
        height: 14px;
    }
    .zp-l-line-loading-custom-image-animated{
        /* #ifndef APP-NVUE */
        animation: loading-circle 1s linear infinite;
        /* #endif */
    }
    .zp-l-circle-loading-view {
        border: 3rpx solid #dddddd;
        border-radius: 50%;
        /* #ifndef APP-NVUE */
        animation: loading-circle 1s linear infinite;
        /* #endif */
        /* #ifdef APP-NVUE */
        width: 30rpx;
        height: 30rpx;
        /* #endif */
    }
    .zp-l-circle-loading-view-rpx {
        margin-right: 8rpx;
        width: 23rpx;
        height: 23rpx;
    }
    .zp-l-circle-loading-view-px {
        margin-right: 4px;
        width: 12px;
        height: 12px;
    }
    .zp-l-text-rpx {
        font-size: 30rpx;
        margin: 0rpx 6rpx;
    }
    .zp-l-text-px {
        font-size: 15px;
        margin: 0px 3px;
    }
    .zp-l-line-rpx {
        height: 1px;
        width: 100rpx;
        margin: 0rpx 10rpx;
    }
    .zp-l-line-px {
        height: 1px;
        width: 50px;
        margin: 0rpx 5px;
    }
    /* #ifndef APP-NVUE */
    @keyframes loading-circle {
        0% {
            -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    /* #endif */
</style>
src/components/z-paging/components/z-paging-refresh.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,214 @@
<!-- [z-paging]下拉刷新view -->
<template>
    <view style="height: 100%;">
        <view :class="showUpdateTime?'zp-r-container zp-r-container-padding':'zp-r-container'">
            <view class="zp-r-left">
                <!-- éžåŠ è½½ä¸­(继续下拉刷新、松手立即刷新状态图片) -->
                <image v-if="status!==R.Loading" :class="leftImageClass" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
                <!-- åŠ è½½çŠ¶æ€å›¾ç‰‡ -->
                <!-- #ifndef APP-NVUE -->
                <image v-else :class="{'zp-line-loading-image':refreshingAnimated,'zp-r-left-image':true,'zp-r-left-image-pre-size-rpx':unit==='rpx','zp-r-left-image-pre-size-px':unit==='px'}" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
                <!-- #endif -->
                <!-- åœ¨nvue中,加载状态loading使用系统loading -->
                <!-- #ifdef APP-NVUE -->
                <view v-else :style="[{'margin-right':showUpdateTime?addUnit(18,unit):addUnit(12, unit)}]">
                    <loading-indicator :class="isIos?{'zp-loading-image-ios-rpx':unit==='rpx','zp-loading-image-ios-px':unit==='px'}:{'zp-loading-image-android-rpx':unit==='rpx','zp-loading-image-android-px':unit==='px'}"
                    :style="[{color:zTheme.indicator[ts]},imgStyle]" :animating="true" />
                </view>
                <!-- #endif -->
            </view>
            <!-- å³ä¾§æ–‡å­—内容 -->
            <view class="zp-r-right">
                <!-- å³ä¾§ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ–‡å­— -->
                <text class="zp-r-right-text" :style="[rightTextStyle,titleStyle]">{{currentTitle}}</text>
                <!-- å³ä¾§ä¸‹æ‹‰åˆ·æ–°æ—¶é—´æ–‡å­— -->
                <text v-if="showUpdateTime&&refresherTimeText.length" class="zp-r-right-text" :class="{'zp-r-right-time-text-rpx':unit==='rpx','zp-r-right-time-text-px':unit==='px'}" :style="[{color:zTheme.title[ts]},updateTimeStyle]">
                    {{refresherTimeText}}
                </text>
            </view>
        </view>
    </view>
</template>
<script>
    import zStatic from '../js/z-paging-static'
    import u from '../js/z-paging-utils'
    import Enum from '../js/z-paging-enum'
    export default {
        name: 'z-paging-refresh',
        data() {
            return {
                R: Enum.Refresher,
                refresherTimeText: '',
                zTheme: {
                    title: { white: '#efefef', black: '#555555' },
                    arrow: { white: zStatic.base64ArrowWhite, black: zStatic.base64Arrow },
                    flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
                    success: { white: zStatic.base64SuccessWhite, black: zStatic.base64Success },
                    indicator: { white: '#eeeeee', black: '#777777' }
                }
            };
        },
        props: ['status', 'defaultThemeStyle', 'defaultText', 'pullingText', 'refreshingText', 'completeText', 'goF2Text', 'defaultImg', 'pullingImg',
            'refreshingImg', 'completeImg', 'refreshingAnimated', 'showUpdateTime', 'updateTimeKey', 'imgStyle', 'titleStyle', 'updateTimeStyle', 'updateTimeTextMap', 'unit', 'isIos'
        ],
        computed: {
            ts() {
                return this.defaultThemeStyle;
            },
            // å½“前状态Map
            statusTextMap() {
                this.updateTime();
                const { R, defaultText, pullingText, refreshingText, completeText, goF2Text } = this;
                return {
                    [R.Default]: defaultText,
                    [R.ReleaseToRefresh]: pullingText,
                    [R.Loading]: refreshingText,
                    [R.Complete]: completeText,
                    [R.GoF2]: goF2Text,
                };
            },
            // å½“前状态文字
            currentTitle() {
                return this.statusTextMap[this.status] || this.defaultText;
            },
            // å·¦ä¾§å›¾ç‰‡class
            leftImageClass() {
                const preSizeClass = `zp-r-left-image-pre-size-${this.unit}`;
                if (this.status === this.R.Complete) return preSizeClass;
                return `zp-r-left-image ${preSizeClass} ${this.status === this.R.Default ? 'zp-r-arrow-down' : 'zp-r-arrow-top'}`;
            },
            // å·¦ä¾§å›¾ç‰‡style
            leftImageStyle() {
                const showUpdateTime = this.showUpdateTime;
                const size = showUpdateTime ? u.addUnit(36, this.unit) : u.addUnit(34, this.unit);
                return {width: size,height: size,'margin-right': showUpdateTime ? u.addUnit(20, this.unit) : u.addUnit(9, this.unit)};
            },
            // å·¦ä¾§å›¾ç‰‡src
            leftImageSrc() {
                const R = this.R;
                const status = this.status;
                if (status === R.Default) {
                    if (!!this.defaultImg) return this.defaultImg;
                    return this.zTheme.arrow[this.ts];
                } else if (status === R.ReleaseToRefresh) {
                    if (!!this.pullingImg) return this.pullingImg;
                    if (!!this.defaultImg) return this.defaultImg;
                    return this.zTheme.arrow[this.ts];
                } else if (status === R.Loading) {
                    if (!!this.refreshingImg) return this.refreshingImg;
                    return this.zTheme.flower[this.ts];;
                } else if (status === R.Complete) {
                    if (!!this.completeImg) return this.completeImg;
                    return this.zTheme.success[this.ts];;
                } else if (status === R.GoF2) {
                    return this.zTheme.arrow[this.ts];
                }
                return '';
            },
            // å³ä¾§æ–‡å­—style
            rightTextStyle() {
                let stl = {};
                // #ifdef APP-NVUE
                const textHeight = this.showUpdateTime ? u.addUnit(40, this.unit) : u.addUnit(80, this.unit);
                stl = {'height': textHeight, 'line-height': textHeight}
                // #endif
                stl['color'] = this.zTheme.title[this.ts];
                stl['font-size'] = u.addUnit(30, this.unit);
                return stl;
            }
        },
        methods: {
            // æ·»åŠ å•ä½
            addUnit(value, unit) {
                return u.addUnit(value, unit);
            },
            // æ›´æ–°ä¸‹æ‹‰åˆ·æ–°æ—¶é—´
            updateTime() {
                if (this.showUpdateTime) {
                    this.refresherTimeText = u.getRefesrherFormatTimeByKey(this.updateTimeKey, this.updateTimeTextMap);
                }
            }
        }
    }
</script>
<style scoped>
    @import "../css/z-paging-static.css";
    .zp-r-container {
        /* #ifndef APP-NVUE */
        display: flex;
        height: 100%;
        /* #endif */
        flex-direction: row;
        justify-content: center;
        align-items: center;
    }
    .zp-r-container-padding {
        /* #ifdef APP-NVUE */
        padding: 7px 0rpx;
        /* #endif */
    }
    .zp-r-left {
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        flex-direction: row;
        align-items: center;
        overflow: hidden;
        /* #ifdef MP-ALIPAY */
        margin-top: -4rpx;
        /* #endif */
    }
    .zp-r-left-image {
        transition-duration: .2s;
        transition-property: transform;
        color: #666666;
    }
    .zp-r-left-image-pre-size-rpx {
        /* #ifndef APP-NVUE */
        width: 34rpx;
        height: 34rpx;
        overflow: hidden;
        /* #endif */
    }
    .zp-r-left-image-pre-size-px {
        /* #ifndef APP-NVUE */
        width: 17px;
        height: 17px;
        overflow: hidden;
        /* #endif */
    }
    .zp-r-arrow-top {
        transform: rotate(0deg);
    }
    .zp-r-arrow-down {
        transform: rotate(180deg);
    }
    .zp-r-right {
        /* #ifndef APP-NVUE */
        display: flex;
        /* #endif */
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
    .zp-r-right-time-text-rpx {
        margin-top: 10rpx;
        font-size: 26rpx;
    }
    .zp-r-right-time-text-px {
        margin-top: 5px;
        font-size: 13px;
    }
</style>
src/components/z-paging/config/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
// z-paging全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
export default {}
src/components/z-paging/css/z-paging-main.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,241 @@
/* [z-paging]公共css*/
.z-paging-content {
    position: relative;
    flex-direction: column;
    /* #ifndef APP-NVUE */
    overflow: hidden;
    /* #endif */
}
.z-paging-content-full {
    /* #ifndef APP-NVUE */
    display: flex;
    width: 100%;
    height: 100%;
    /* #endif */
}
.z-paging-content-fixed, .zp-loading-fixed {
    position: fixed;
    /* #ifndef APP-NVUE */
    height: auto;
    width: auto;
    /* #endif */
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}
.zp-f2-content {
    width: 100%;
    position: fixed;
    top: 0;
    left: 0;
    background-color: white;
}
.zp-page-top, .zp-page-bottom {
    /* #ifndef APP-NVUE */
    width: auto;
    /* #endif */
    position: fixed;
    left: 0;
    right: 0;
    z-index: 999;
}
.zp-page-left, .zp-page-right {
    /* #ifndef APP-NVUE */
    height: 100%;
    /* #endif */
}
.zp-scroll-view-super {
    flex: 1;
    overflow: hidden;
    position: relative;
}
.zp-view-super {
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: row;
}
.zp-scroll-view-container, .zp-scroll-view {
    position: relative;
    /* #ifndef APP-NVUE */
    height: 100%;
    width: 100%;
    /* #endif */
}
.zp-absoulte {
    /* #ifndef APP-NVUE */
    position: absolute;
    top: 0;
    width: auto;
    /* #endif */
}
.zp-scroll-view-absolute {
    position: absolute;
    top: 0;
    left: 0;
}
/* #ifndef APP-NVUE */
.zp-scroll-view-hide-scrollbar ::-webkit-scrollbar {
    display: none;
    -webkit-appearance: none;
    width: 0 !important;
    height: 0 !important;
    background: transparent;
}
/* #endif */
.zp-paging-touch-view {
    width: 100%;
    height: 100%;
    position: relative;
}
.zp-fixed-bac-view {
    position: absolute;
    width: 100%;
    top: 0;
    left: 0;
    height: 200px;
}
.zp-paging-main {
    height: 100%;
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: column;
}
.zp-paging-container {
    flex: 1;
    position: relative;
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: column;
}
.zp-chat-record-loading-custom-image {
    width: 35rpx;
    height: 35rpx;
    /* #ifndef APP-NVUE */
    animation: loading-flower 1s linear infinite;
    /* #endif */
}
.zp-page-bottom-keyboard-placeholder-animate {
    transition-property: height;
    transition-duration: 0.15s;
    /* #ifndef APP-NVUE */
    will-change: height;
    /* #endif */
}
.zp-custom-refresher-container {
    overflow: hidden;
}
.zp-custom-refresher-refresh {
    /* #ifndef APP-NVUE */
    display: block;
    /* #endif */
}
.zp-back-to-top {
    z-index: 999;
    position: absolute;
    bottom: 0rpx;
    transition-duration: .3s;
    transition-property: opacity;
}
.zp-back-to-top-rpx {
    width: 76rpx;
    height: 76rpx;
    bottom: 0rpx;
    right: 25rpx;
}
.zp-back-to-top-px {
    width: 38px;
    height: 38px;
    bottom: 0px;
    right: 13px;
}
.zp-back-to-top-show {
    opacity: 1;
}
.zp-back-to-top-hide {
    opacity: 0;
}
.zp-back-to-top-img {
    /* #ifndef APP-NVUE */
    width: 100%;
    height: 100%;
    /* #endif */
    /* #ifdef APP-NVUE */
    flex: 1;
    /* #endif */
    z-index: 999;
}
.zp-back-to-top-img-inversion {
    transform: rotate(180deg);
}
.zp-empty-view {
    /* #ifdef APP-NVUE */
    height: 100%;
    /* #endif */
    flex: 1;
}
.zp-empty-view-center {
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.zp-loading-fixed {
    z-index: 9999;
}
.zp-safe-area-inset-bottom {
    position: absolute;
    /* #ifndef APP-PLUS */
    height: env(safe-area-inset-bottom);
    /* #endif */
}
.zp-n-refresh-container {
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    justify-content: center;
    width: 750rpx;
}
.zp-n-list-container{
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: row;
    flex: 1;
}
src/components/z-paging/css/z-paging-static.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
/* [z-paging]公用的静态css资源 */
.zp-line-loading-image {
    /* #ifndef APP-NVUE */
    animation: loading-flower 1s steps(12) infinite;
    /* #endif */
    color: #666666;
}
.zp-line-loading-image-rpx {
    margin-right: 8rpx;
    width: 34rpx;
    height: 34rpx;
}
.zp-line-loading-image-px {
    margin-right: 4px;
    width: 17px;
    height: 17px;
}
.zp-loading-image-ios-rpx {
    width: 40rpx;
    height: 40rpx;
}
.zp-loading-image-ios-px {
    width: 20px;
    height: 20px;
}
.zp-loading-image-android-rpx {
    width: 34rpx;
    height: 34rpx;
}
.zp-loading-image-android-px {
    width: 17px;
    height: 17px;
}
/* #ifndef APP-NVUE */
@keyframes loading-flower {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    to {
        -webkit-transform: rotate(1turn);
        transform: rotate(1turn);
    }
}
/* #endif */
src/components/z-paging/i18n/en.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
{
    "zp.refresher.default": "Pull down to refresh",
    "zp.refresher.pulling": "Release to refresh",
    "zp.refresher.refreshing": "Refreshing...",
    "zp.refresher.complete": "Refresh succeeded",
    "zp.refresher.f2": "Refresh to enter 2f",
    "zp.loadingMore.default": "Click to load more",
    "zp.loadingMore.loading": "Loading...",
    "zp.loadingMore.noMore": "No more data",
    "zp.loadingMore.fail": "Load failed,click to reload",
    "zp.emptyView.title": "No data",
    "zp.emptyView.reload": "Reload",
    "zp.emptyView.error": "Sorry,load failed",
    "zp.refresherUpdateTime.title": "Last update: ",
    "zp.refresherUpdateTime.none": "None",
    "zp.refresherUpdateTime.today": "Today",
    "zp.refresherUpdateTime.yesterday": "Yesterday",
    "zp.systemLoading.title": "Loading..."
}
src/components/z-paging/i18n/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
    en,
    'zh-Hans': zhHans,
    'zh-Hant': zhHant
}
src/components/z-paging/i18n/zh-Hans.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
{
    "zp.refresher.default": "继续下拉刷新",
    "zp.refresher.pulling": "松开立即刷新",
    "zp.refresher.refreshing": "正在刷新...",
    "zp.refresher.complete": "刷新成功",
    "zp.refresher.f2": "松手进入二楼",
    "zp.loadingMore.default": "点击加载更多",
    "zp.loadingMore.loading": "正在加载...",
    "zp.loadingMore.noMore": "没有更多了",
    "zp.loadingMore.fail": "加载失败,点击重新加载",
    "zp.emptyView.title": "没有数据哦~",
    "zp.emptyView.reload": "重新加载",
    "zp.emptyView.error": "很抱歉,加载失败",
    "zp.refresherUpdateTime.title": "最后更新:",
    "zp.refresherUpdateTime.none": "无",
    "zp.refresherUpdateTime.today": "今天",
    "zp.refresherUpdateTime.yesterday": "昨天",
    "zp.systemLoading.title": "加载中..."
}
src/components/z-paging/i18n/zh-Hant.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
{
    "zp.refresher.default": "繼續下拉重繪",
    "zp.refresher.pulling": "鬆開立即重繪",
    "zp.refresher.refreshing": "正在重繪...",
    "zp.refresher.complete": "重繪成功",
    "zp.refresher.f2": "鬆手進入二樓",
    "zp.loadingMore.default": "點擊加載更多",
    "zp.loadingMore.loading": "正在加載...",
    "zp.loadingMore.noMore": "沒有更多了",
    "zp.loadingMore.fail": "加載失敗,點擊重新加載",
    "zp.emptyView.title": "沒有數據哦~",
    "zp.emptyView.reload": "重新加載",
    "zp.emptyView.error": "很抱歉,加載失敗",
    "zp.refresherUpdateTime.title": "最後更新:",
    "zp.refresherUpdateTime.none": "無",
    "zp.refresherUpdateTime.today": "今天",
    "zp.refresherUpdateTime.yesterday": "昨天",
    "zp.systemLoading.title": "加載中..."
}
src/components/z-paging/js/hooks/useZPaging.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
// [z-paging]useZPaging hooks
import { onPageScroll, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
function useZPaging(paging) {
    const cPaging = !!paging ? paging.value || paging : null;
    onPullDownRefresh(() => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.reload().catch(() => {});
    })
    onPageScroll(e => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.updatePageScrollTop(e.scrollTop);
        e.scrollTop < 10 && cPaging.value.doChatRecordLoadMore();
    })
    onReachBottom(() => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.pageReachBottom();
    })
}
export default useZPaging
src/components/z-paging/js/hooks/useZPagingComp.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
// [z-paging]useZPagingComp hooks
function useZPagingComp(paging) {
    const cPaging = !!paging ? paging.value || paging : null;
    const reload = () => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.reload().catch(() => {});
    }
    const updatePageScrollTop = scrollTop => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.updatePageScrollTop(scrollTop);
    }
    const doChatRecordLoadMore = () => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.doChatRecordLoadMore();
    }
    const pageReachBottom = () => {
        if (!cPaging || !cPaging.value) return;
        cPaging.value.pageReachBottom();
    }
    return { reload, updatePageScrollTop, doChatRecordLoadMore, pageReachBottom };
}
export default useZPagingComp
src/components/z-paging/js/modules/back-to-top.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
// [z-paging]点击返回顶部view模块
import u from '.././z-paging-utils'
export default {
    props: {
        // è‡ªåŠ¨æ˜¾ç¤ºç‚¹å‡»è¿”å›žé¡¶éƒ¨æŒ‰é’®ï¼Œé»˜è®¤ä¸ºå¦
        autoShowBackToTop: {
            type: Boolean,
            default: u.gc('autoShowBackToTop', false)
        },
        // ç‚¹å‡»è¿”回顶部按钮显示/隐藏的阈值(滚动距离),单位为px,默认为400rpx
        backToTopThreshold: {
            type: [Number, String],
            default: u.gc('backToTopThreshold', '400rpx')
        },
        // ç‚¹å‡»è¿”回顶部按钮的自定义图片地址,默认使用z-paging内置的图片
        backToTopImg: {
            type: String,
            default: u.gc('backToTopImg', '')
        },
        // ç‚¹å‡»è¿”回顶部按钮返回到顶部时是否展示过渡动画,默认为是
        backToTopWithAnimate: {
            type: Boolean,
            default: u.gc('backToTopWithAnimate', true)
        },
        // ç‚¹å‡»è¿”回顶部按钮与底部的距离,注意添加单位px或rpx,默认为160rpx
        backToTopBottom: {
            type: [Number, String],
            default: u.gc('backToTopBottom', '160rpx')
        },
        // ç‚¹å‡»è¿”回顶部按钮的自定义样式
        backToTopStyle: {
            type: Object,
            default: u.gc('backToTopStyle', {}),
        },
        // iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向,默认为是
        enableBackToTop: {
            type: Boolean,
            default: u.gc('enableBackToTop', true)
        },
    },
    data() {
        return {
            // ç‚¹å‡»è¿”回顶部的class
            backToTopClass: 'zp-back-to-top zp-back-to-top-hide',
            // ä¸Šæ¬¡ç‚¹å‡»è¿”回顶部的时间
            lastBackToTopShowTime: 0,
            // ç‚¹å‡»è¿”回顶部显示的class是否在展示中,使得按钮展示/隐藏过度效果更自然
            showBackToTopClass: false,
        }
    },
    computed: {
        backToTopThresholdUnitConverted() {
            return u.addUnit(this.backToTopThreshold, this.unit);
        },
        backToTopBottomUnitConverted() {
            return u.addUnit(this.backToTopBottom, this.unit);
        },
        finalEnableBackToTop() {
            return this.usePageScroll ? false : this.enableBackToTop;
        },
        finalBackToTopThreshold() {
            return u.convertToPx(this.backToTopThresholdUnitConverted);
        },
        finalBackToTopStyle() {
            const backToTopStyle = this.backToTopStyle;
            if (!backToTopStyle.bottom) {
                backToTopStyle.bottom = this.windowBottom + u.convertToPx(this.backToTopBottomUnitConverted) + 'px';
            }
            if(!backToTopStyle.position){
                backToTopStyle.position = this.usePageScroll ? 'fixed': 'absolute';
            }
            return backToTopStyle;
        },
        finalBackToTopClass() {
            return `${this.backToTopClass} zp-back-to-top-${this.unit}`;
        }
    },
    methods: {
        // ç‚¹å‡»äº†è¿”回顶部
        _backToTopClick() {
            let callbacked = false;
            this.$emit('backToTopClick', toTop => {
                (toTop === undefined || toTop === true) && this._handleToTop();
                callbacked = true;
            });
            // å¦‚果用户没有禁止默认的返回顶部事件,则触发滚动到顶部
            this.$nextTick(() => {
                !callbacked && this._handleToTop();
            })
        },
        // å¤„理滚动到顶部(聊天记录模式中为滚动到底部)
        _handleToTop() {
            !this.backToTopWithAnimate && this._checkShouldShowBackToTop(0);
            !this.useChatRecordMode ? this.scrollToTop(this.backToTopWithAnimate) : this.scrollToBottom(this.backToTopWithAnimate);
        },
        // åˆ¤æ–­æ˜¯å¦è¦æ˜¾ç¤ºè¿”回顶部按钮
        _checkShouldShowBackToTop(scrollTop) {
            if (!this.autoShowBackToTop) {
                this.showBackToTopClass = false;
                return;
            }
            if (scrollTop > this.finalBackToTopThreshold) {
                if (!this.showBackToTopClass) {
                    // è®°å½•当前点击返回顶部按钮显示的class生效了
                    this.showBackToTopClass = true;
                    this.lastBackToTopShowTime = new Date().getTime();
                    // å½“滚动到需要展示返回顶部的阈值内,则延迟300毫秒展示返回到顶部按钮
                    u.delay(() => {
                        this.backToTopClass = 'zp-back-to-top zp-back-to-top-show';
                    }, 300)
                }
            } else {
                // å¦‚果当前点击返回顶部按钮显示的class是生效状态并且滚动小于触发阈值,则隐藏返回顶部按钮
                if (this.showBackToTopClass) {
                    this.backToTopClass = 'zp-back-to-top zp-back-to-top-hide';
                    u.delay(() => {
                        this.showBackToTopClass = false;
                    }, new Date().getTime() - this.lastBackToTopShowTime < 500 ? 0 : 300)
                }
            }
        },
    }
}
src/components/z-paging/js/modules/chat-record-mode.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,149 @@
// [z-paging]聊天记录模式模块
import u from '.././z-paging-utils'
export default {
    props: {
        // ä½¿ç”¨èŠå¤©è®°å½•模式,默认为否
        useChatRecordMode: {
            type: Boolean,
            default: u.gc('useChatRecordMode', false)
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式时滚动到顶部后,列表垂直移动偏移距离。默认0rpx。单位px(暂时无效)
        chatRecordMoreOffset: {
            type: [Number, String],
            default: u.gc('chatRecordMoreOffset', '0rpx')
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式时是否自动隐藏键盘:在用户触摸列表时候自动隐藏键盘,默认为是
        autoHideKeyboardWhenChat: {
            type: Boolean,
            default: u.gc('autoHideKeyboardWhenChat', true)
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时是否自动调整slot="bottom"高度,默认为是
        autoAdjustPositionWhenChat: {
            type: Boolean,
            default: u.gc('autoAdjustPositionWhenChat', true)
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时占位高度偏移距离。默认0rpx。单位px
        chatAdjustPositionOffset: {
            type: [Number, String],
            default: u.gc('chatAdjustPositionOffset', '0rpx')
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时是否自动滚动到底部,默认为否
        autoToBottomWhenChat: {
            type: Boolean,
            default: u.gc('autoToBottomWhenChat', false)
        },
        // ä½¿ç”¨èŠå¤©è®°å½•模式中reload时是否显示chatLoading,默认为否
        showChatLoadingWhenReload: {
            type: Boolean,
            default: u.gc('showChatLoadingWhenReload', false)
        },
        // åœ¨èŠå¤©è®°å½•模式中滑动到顶部状态为默认状态时,以加载中的状态展示,默认为是。若设置为否,则默认会显示【点击加载更多】,然后才会显示loading
        chatLoadingMoreDefaultAsLoading: {
            type: Boolean,
            default: u.gc('chatLoadingMoreDefaultAsLoading', true)
        },
    },
    data() {
        return {
            // é”®ç›˜é«˜åº¦
            keyboardHeight: 0,
            // é”®ç›˜é«˜åº¦æ˜¯å¦æœªæ”¹å˜ï¼Œæ­¤æ—¶å ä½é«˜åº¦å˜åŒ–不需要动画效果
            isKeyboardHeightChanged: false,
        }
    },
    computed: {
        finalChatRecordMoreOffset() {
            return u.convertToPx(this.chatRecordMoreOffset);
        },
        finalChatAdjustPositionOffset() {
            return u.convertToPx(this.chatAdjustPositionOffset);
        },
        // èŠå¤©è®°å½•模式旋转180度style
        chatRecordRotateStyle() {
            let cellStyle;
            // åœ¨vue中,直接将列表倒置,因此在vue的cell中,也直接写style="transform: scaleY(-1)"转回来即可。
            // #ifndef APP-NVUE
            cellStyle = this.useChatRecordMode ? { transform: 'scaleY(-1)' } : {};
            // #endif
            // åœ¨nvue中,需要考虑数据量不满一页的情况,因为nvue中的list无法通过flex-end修改不满一页的起始位置,会导致不满一页时列表数据从底部开始,因此需要特别判断
            // å½“数据不满一屏的时候,不进行列表倒置
            // #ifdef APP-NVUE
            cellStyle = this.useChatRecordMode ? { transform: this.isFirstPageAndNoMore ? 'scaleY(1)' : 'scaleY(-1)' } : {};
            // #endif
            this.$emit('update:cellStyle', cellStyle);
            this.$emit('cellStyleChange', cellStyle);
            // åœ¨èŠå¤©è®°å½•模式中,如果列表没有倒置并且当前是第一页,则需要自动滚动到最底部
            this.$nextTick(() => {
                if (this.isFirstPage && this.isChatRecordModeAndNotInversion) {
                    this.$nextTick(() => {
                        // è¿™é‡Œå¤šæ¬¡è§¦å‘滚动到底部是为了避免在某些情况下,即使是在nextTick但是cell未渲染完毕导致滚动到底部位置不正确的问题
                        this._scrollToBottom(false);
                        u.delay(() => {
                            this._scrollToBottom(false);
                            u.delay(() => {
                                this._scrollToBottom(false);
                            }, 50)
                        }, 50)
                    })
                }
            })
            return cellStyle;
        },
        // æ˜¯å¦æ˜¯èŠå¤©è®°å½•列表并且有配置transform
        isChatRecordModeHasTransform() {
            return this.useChatRecordMode && this.chatRecordRotateStyle && this.chatRecordRotateStyle.transform;
        },
        // æ˜¯å¦æ˜¯èŠå¤©è®°å½•列表并且列表未倒置
        isChatRecordModeAndNotInversion() {
            return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(1)';
        },
        // æ˜¯å¦æ˜¯èŠå¤©è®°å½•列表并且列表倒置
        isChatRecordModeAndInversion() {
            return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(-1)';
        },
        // æœ€ç»ˆçš„聊天记录模式中底部安全区域的高度,如果开启了底部安全区域并且键盘未弹出,则添加底部区域高度
        chatRecordModeSafeAreaBottom() {
            return this.safeAreaInsetBottom && !this.keyboardHeight ? this.safeAreaBottom : 0;
        }
    },
    mounted() {
        // ç›‘听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
        // #ifndef H5 || MP-BAIDU || MP-TOUTIAO
        if (this.useChatRecordMode) {
            uni.onKeyboardHeightChange(this._handleKeyboardHeightChange);
        }
        // #endif
    },
    methods: {
        // æ·»åŠ èŠå¤©è®°å½•
        addChatRecordData(data, toBottom = true, toBottomWithAnimate = true) {
            if (!this.useChatRecordMode) return;
            this.isTotalChangeFromAddData = true;
            this.addDataFromTop(data, toBottom, toBottomWithAnimate);
        },
        // æ‰‹åŠ¨è§¦å‘æ»šåŠ¨åˆ°é¡¶éƒ¨åŠ è½½æ›´å¤šï¼ŒèŠå¤©è®°å½•æ¨¡å¼æ—¶æœ‰æ•ˆ
        doChatRecordLoadMore() {
            this.useChatRecordMode && this._onLoadingMore('click');
        },
        // å¤„理键盘高度变化
        _handleKeyboardHeightChange(res) {
            this.$emit('keyboardHeightChange', res);
            if (this.autoAdjustPositionWhenChat) {
                this.isKeyboardHeightChanged = true;
                this.keyboardHeight = res.height > 0 ? res.height + this.finalChatAdjustPositionOffset : res.height;
            }
            if (this.autoToBottomWhenChat && this.keyboardHeight > 0) {
                u.delay(() => {
                    this.scrollToBottom(false);
                    u.delay(() => {
                        this.scrollToBottom(false);
                    })
                })
            }
        }
    }
}
src/components/z-paging/js/modules/common-layout.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
// [z-paging]通用布局相关模块
import u from '.././z-paging-utils'
// #ifdef APP-NVUE
const weexDom = weex.requireModule('dom');
// #endif
export default {
    data() {
        return {
            systemInfo: null,
            cssSafeAreaInsetBottom: -1,
            isReadyDestroy: false,
        }
    },
    computed: {
        // é¡¶éƒ¨å¯ç”¨è·ç¦»
        windowTop() {
            if (!this.systemInfo) return 0;
            // æš‚时修复vue3中隐藏系统导航栏后windowTop获取不正确的问题,具体bug详见https://ask.dcloud.net.cn/question/141634
            // æ„Ÿè°¢litangyu!!https://github.com/SmileZXLee/uni-z-paging/issues/25
            // #ifdef VUE3 && H5
            const pageHeadNode = document.getElementsByTagName("uni-page-head");
            if (!pageHeadNode.length) return 0;
            // #endif
            return this.systemInfo.windowTop || 0;
        },
        // åº•部安全区域高度
        safeAreaBottom() {
            if (!this.systemInfo) return 0;
            let safeAreaBottom = 0;
            // #ifdef APP-PLUS
            safeAreaBottom = this.systemInfo.safeAreaInsets.bottom || 0 ;
            // #endif
            // #ifndef APP-PLUS
            safeAreaBottom = Math.max(this.cssSafeAreaInsetBottom, 0);
            // #endif
            return safeAreaBottom;
        },
        // æ˜¯å¦æ˜¯æ¯”较老的webview,在一些老的webview中,需要进行一些特殊处理
        isOldWebView() {
            // #ifndef APP-NVUE || MP-KUAISHOU
            try {
                const systemInfos = u.getSystemInfoSync(true).system.split(' ');
                const deviceType = systemInfos[0];
                const version = parseInt(systemInfos[1]);
                if ((deviceType === 'iOS' && version <= 10) || (deviceType === 'Android' && version <= 6)) {
                    return true;
                }
            } catch(e) {
                return false;
            }
            // #endif
            return false;
        },
        // å½“前组件的$slots,兼容不同平台
        zSlots() {
            // #ifdef VUE2
            // #ifdef MP-ALIPAY
            return this.$slots;
            // #endif
            return this.$scopedSlots || this.$slots;
            // #endif
            return this.$slots;
        },
    },
    beforeDestroy() {
        this.isReadyDestroy = true;
    },
    // #ifdef VUE3
    unmounted() {
        this.isReadyDestroy = true;
    },
    // #endif
    methods: {
        // æ›´æ–°fixed模式下z-paging的布局
        updateFixedLayout() {
            this.fixed && this.$nextTick(() => {
                this.systemInfo = u.getSystemInfoSync();
            })
        },
        // èŽ·å–èŠ‚ç‚¹å°ºå¯¸
        _getNodeClientRect(select, inDom = true, scrollOffset = false) {
            if (this.isReadyDestroy) {
                return Promise.resolve(false);
            };
            // nvue中获取节点信息
            // #ifdef APP-NVUE
            select = select.replace(/[.|#]/g, '');
            const ref = this.$refs[select];
            return new Promise((resolve, reject) => {
                if (ref) {
                    weexDom.getComponentRect(ref, option => {
                        resolve(option && option.result ? [option.size] : false);
                    })
                } else {
                    resolve(false);
                }
            });
            return;
            // #endif
            // vue中获取节点信息
            //#ifdef MP-ALIPAY
            inDom = false;
            //#endif
            /*
            inDom可能是true、false,也可能是具体的dom节点
            å¦‚æžœinDom不为false,则使用uni.createSelectorQuery().in()进行查询,如果inDom为true,则in中的是this,否则in中的为具体的dom
            å¦‚æžœinDom为false,则使用uni.createSelectorQuery()进行查询
            */
            let res = !!inDom ? uni.createSelectorQuery().in(inDom === true ? this : inDom) : uni.createSelectorQuery();
            scrollOffset ? res.select(select).scrollOffset() : res.select(select).boundingClientRect();
            return new Promise((resolve, reject) => {
                res.exec(data => {
                    resolve((data && data != '' && data != undefined && data.length) ? data : false);
                });
            });
        },
        // èŽ·å–slot="left"和slot="right"宽度并且更新布局
        _updateLeftAndRightWidth(targetStyle, parentNodePrefix) {
            this.$nextTick(() => {
                let delayTime = 0;
                // #ifdef MP-BAIDU
                delayTime = 10;
                // #endif
                setTimeout(() => {
                    ['left','right'].map(position => {
                        this._getNodeClientRect(`.${parentNodePrefix}-${position}`).then(res => {
                            this.$set(targetStyle, position, res ? res[0].width + 'px' : '0px');
                        });
                    })
                }, delayTime)
            })
        },
        // é€šè¿‡èŽ·å–css设置的底部安全区域占位view高度设置bottom距离(直接通过systemInfo在部分平台上无法获取到底部安全区域)
        _getCssSafeAreaInsetBottom(success) {
            this._getNodeClientRect('.zp-safe-area-inset-bottom').then(res => {
                this.cssSafeAreaInsetBottom = res ? res[0].height : -1;
                res && success && success();
            });
        },
        // åŒæ­¥èŽ·å–ç³»ç»Ÿä¿¡æ¯ï¼Œå…¼å®¹ä¸åŒå¹³å°ï¼ˆä¾›z-paging-swiper使用)
        _getSystemInfoSync(useCache = false) {
            return u.getSystemInfoSync(useCache);
        }
    }
}
src/components/z-paging/js/modules/data-handle.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,736 @@
// [z-paging]数据处理模块
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import Enum from '.././z-paging-enum'
import interceptor from '../z-paging-interceptor'
export default {
    props: {
        // è‡ªå®šä¹‰åˆå§‹çš„pageNo,默认为1
        defaultPageNo: {
            type: Number,
            default: u.gc('defaultPageNo', 1),
            observer: function(newVal) {
                this.pageNo = newVal;
            },
        },
        // è‡ªå®šä¹‰pageSize,默认为10
        defaultPageSize: {
            type: Number,
            default: u.gc('defaultPageSize', 10),
            validator: (value) => {
                if (value <= 0) u.consoleErr('default-page-size必须大于0!');
                return value > 0;
            }
        },
        // ä¸ºä¿è¯æ•°æ®ä¸€è‡´ï¼Œè®¾ç½®å½“前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
        dataKey: {
            type: [Number, String, Object],
            default: u.gc('dataKey', null),
        },
        // ä½¿ç”¨ç¼“存,若开启将自动缓存第一页的数据,默认为否。请注意,因考虑到切换tab时不同tab数据不同的情况,默认仅会缓存组件首次加载时第一次请求到的数据,后续的下拉刷新操作不会更新缓存。
        useCache: {
            type: Boolean,
            default: u.gc('useCache', false)
        },
        // ä½¿ç”¨ç¼“存时缓存的key,用于区分不同列表的缓存数据,useCache为true时必须设置,否则缓存无效
        cacheKey: {
            type: String,
            default: u.gc('cacheKey', null)
        },
        // ç¼“存模式,默认仅会缓存组件首次加载时第一次请求到的数据,可设置为always,即代表总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
        cacheMode: {
            type: String,
            default: u.gc('cacheMode', Enum.CacheMode.Default)
        },
        // è‡ªåŠ¨æ³¨å…¥çš„list名,可自动修改父view(包含ref="paging")中对应name的list值
        autowireListName: {
            type: String,
            default: u.gc('autowireListName', '')
        },
        // è‡ªåŠ¨æ³¨å…¥çš„query名,可自动调用父view(包含ref="paging")中的query方法
        autowireQueryName: {
            type: String,
            default: u.gc('autowireQueryName', '')
        },
        // èŽ·å–åˆ†é¡µæ•°æ®Function,功能与@query类似。若设置了fetch则@query将不再触发
        fetch: {
            type: Function,
            default: null
        },
        // fetch的附加参数,fetch配置后有效
        fetchParams: {
            type: Object,
            default: u.gc('fetchParams', null)
        },
        // z-paging mounted后自动调用reload方法(mounted后自动调用接口),默认为是
        auto: {
            type: Boolean,
            default: u.gc('auto', true)
        },
        // ç”¨æˆ·ä¸‹æ‹‰åˆ·æ–°æ—¶æ˜¯å¦è§¦å‘reload方法,默认为是
        reloadWhenRefresh: {
            type: Boolean,
            default: u.gc('reloadWhenRefresh', true)
        },
        // reload时自动滚动到顶部,默认为是
        autoScrollToTopWhenReload: {
            type: Boolean,
            default: u.gc('autoScrollToTopWhenReload', true)
        },
        // reload时立即自动清空原list,默认为是,若立即自动清空,则在reload之后、请求回调之前页面是空白的
        autoCleanListWhenReload: {
            type: Boolean,
            default: u.gc('autoCleanListWhenReload', true)
        },
        // åˆ—表刷新时自动显示下拉刷新view,默认为否
        showRefresherWhenReload: {
            type: Boolean,
            default: u.gc('showRefresherWhenReload', false)
        },
        // åˆ—表刷新时自动显示加载更多view,且为加载中状态,默认为否
        showLoadingMoreWhenReload: {
            type: Boolean,
            default: u.gc('showLoadingMoreWhenReload', false)
        },
        // ç»„ä»¶created时立即触发reload(可解决一些情况下先看到页面再看到loading的问题),auto为true时有效。为否时将在mounted+nextTick后触发reload,默认为否
        createdReload: {
            type: Boolean,
            default: u.gc('createdReload', false)
        },
        // æœ¬åœ°åˆ†é¡µæ—¶ä¸Šæ‹‰åŠ è½½æ›´å¤šå»¶è¿Ÿæ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤200毫秒
        localPagingLoadingTime: {
            type: [Number, String],
            default: u.gc('localPagingLoadingTime', 200)
        },
        // è‡ªåŠ¨æ‹¼æŽ¥complete中传过来的数组(使用聊天记录模式时无效)
        concat: {
            type: Boolean,
            default: u.gc('concat', true)
        },
        // è¯·æ±‚失败是否触发reject,默认为是
        callNetworkReject: {
            type: Boolean,
            default: u.gc('callNetworkReject', true)
        },
        // çˆ¶ç»„ä»¶v-model所绑定的list的值
        value: {
            type: Array,
            default: function() {
                return [];
            }
        },
        // #ifdef VUE3
        modelValue: {
            type: Array,
            default: function() {
                return [];
            }
        }
        // #endif
    },
    data (){
        return {
            currentData: [],
            totalData: [],
            realTotalData: [],
            totalLocalPagingList: [],
            dataPromiseResultMap: {
                reload: null,
                complete: null,
                localPaging: null
            },
            isSettingCacheList: false,
            pageNo: 1,
            currentRefreshPageSize: 0,
            isLocalPaging: false,
            isAddedData: false,
            isTotalChangeFromAddData: false,
            privateConcat: true,
            myParentQuery: -1,
            firstPageLoaded: false,
            pagingLoaded: false,
            loaded: false,
            isUserReload: true,
            fromEmptyViewReload: false,
            queryFrom: '',
            listRendering: false,
            isHandlingRefreshToPage: false,
            isFirstPageAndNoMore: false,
            totalDataChangeThrow: true
        }
    },
    computed: {
        pageSize() {
            return this.defaultPageSize;
        },
        finalConcat() {
            return this.concat && this.privateConcat;
        },
        finalUseCache() {
            if (this.useCache && !this.cacheKey) {
                u.consoleErr('use-cache为true时,必须设置cache-key,否则缓存无效!');
            }
            return this.useCache && !!this.cacheKey;
        },
        finalCacheKey() {
            return this.cacheKey ? `${c.cachePrefixKey}-${this.cacheKey}` : null;
        },
        isFirstPage() {
            return this.pageNo === this.defaultPageNo;
        }
    },
    watch: {
        totalData(newVal, oldVal) {
            this._totalDataChange(newVal, oldVal, this.totalDataChangeThrow);
            this.totalDataChangeThrow = true;
        },
        currentData(newVal, oldVal) {
            this._currentDataChange(newVal, oldVal);
        },
        useChatRecordMode(newVal, oldVal) {
            if (newVal) {
                this.nLoadingMoreFixedHeight = false;
            }
        },
        value: {
            handler(newVal) {
                // å½“v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
                if (newVal !== this.totalData) {
                    this.totalDataChangeThrow = false;
                    this.totalData = newVal;
                }
            },
            immediate: true
        },
        // #ifdef VUE3
        modelValue: {
            handler(newVal) {
                // å½“v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
                if (newVal !== this.totalData) {
                    this.totalDataChangeThrow = false;
                    this.totalData = newVal;
                }
            },
            immediate: true
        }
        // #endif
    },
    methods: {
        // è¯·æ±‚结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否成功(默认为是)
        complete(data, success = true) {
            this.customNoMore = -1;
            return this.addData(data, success);
        },
        //【保证数据一致】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为dataKey,需与:data-key绑定的一致,第三个参数为是否成功(默认为是)
        completeByKey(data, dataKey = null, success = true) {
            if (dataKey !== null && this.dataKey !== null && dataKey !== this.dataKey) {
                this.isFirstPage && this.endRefresh();
                return new Promise(resolve => resolve());
            }
            this.customNoMore = -1;
            return this.addData(data, success);
        },
        //【通过total判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为total(列表总数),第三个参数为是否成功(默认为是)
        completeByTotal(data, total, success = true) {
            if (total == 'undefined') {
                this.customNoMore = -1;
            } else {
                const dataTypeRes = this._checkDataType(data, success, false);
                data = dataTypeRes.data;
                success = dataTypeRes.success;
                if (total >= 0 && success) {
                    return new Promise((resolve, reject) => {
                        this.$nextTick(() => {
                            let nomore = false;
                            const realTotalDataCount = this.pageNo == this.defaultPageNo ? 0 : this.realTotalData.length;
                            const dataLength = this.privateConcat ? data.length : 0;
                            let exceedCount = realTotalDataCount + dataLength - total;
                            // æ²¡æœ‰æ›´å¤šæ•°æ®äº†
                            if (exceedCount >= 0) {
                                nomore = true;
                                // ä»…截取total内部分的数据
                                exceedCount = this.defaultPageSize - exceedCount;
                                if (this.privateConcat && exceedCount > 0 && exceedCount < data.length) {
                                    data = data.splice(0, exceedCount);
                                }
                            }
                            this.completeByNoMore(data, nomore, success).then(res => resolve(res)).catch(() => reject());
                        })
                    });
                }
            }
            return this.addData(data, success);
        },
        //【自行判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否没有更多数据,第三个参数为是否成功(默认是是)
        completeByNoMore(data, nomore, success = true) {
            if (nomore != 'undefined') {
                this.customNoMore = nomore == true ? 1 : 0;
            }
            return this.addData(data, success);
        },
        // è¯·æ±‚结束且请求失败时调用,支持传入请求失败原因
        completeByError(errorMsg) {
            this.customerEmptyViewErrorText = errorMsg;
            return this.complete(false);
        },
        // ä¸Žä¸Šæ–¹complete方法功能一致,新版本中设置服务端回调数组请使用complete方法
        addData(data, success = true) {
            if (!this.fromCompleteEmit) {
                this.disabledCompleteEmit = true;
                this.fromCompleteEmit = false;
            }
            const currentTimeStamp = u.getTime();
            const disTime = currentTimeStamp - this.requestTimeStamp;
            let minDelay = this.minDelay;
            if (this.isFirstPage && this.finalShowRefresherWhenReload) {
                minDelay = Math.max(400, minDelay);
            }
            const addDataDalay = (this.requestTimeStamp > 0 && disTime < minDelay) ? minDelay - disTime : 0;
            this.$nextTick(() => {
                u.delay(() => {
                    this._addData(data, success, false);
                }, this.delay > 0 ? this.delay : addDataDalay)
            })
            return new Promise((resolve, reject) => {
                this.dataPromiseResultMap.complete = { resolve, reject };
            });
        },
        // ä»Žé¡¶éƒ¨æ·»åŠ æ•°æ®ï¼Œä¸ä¼šå½±å“åˆ†é¡µçš„pageNo和pageSize
        addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
            // æ•°æ®æ˜¯å¦æ‹¼æŽ¥åˆ°é¡¶éƒ¨ï¼Œå¦‚果是聊天记录模式并且列表没有倒置,则应该拼接在底部
            let addFromTop = !this.isChatRecordModeAndNotInversion;
            data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : (addFromTop ? data.reverse() : data);
            // #ifndef APP-NVUE
            this.finalUseVirtualList && this._setCellIndex(data, 'top')
            // #endif
            this.totalData = addFromTop ? [...data, ...this.totalData] : [...this.totalData, ...data];
            if (toTop) {
                u.delay(() => this.useChatRecordMode ? this.scrollToBottom(toTopWithAnimate) : this.scrollToTop(toTopWithAnimate));
            }
        },
        // é‡æ–°è®¾ç½®åˆ—表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
        resetTotalData(data) {
            this.isTotalChangeFromAddData = true;
            data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : data;
            this.totalData = data;
        },
        // è®¾ç½®æœ¬åœ°åˆ†é¡µæ•°æ®ï¼Œè¯·æ±‚结束(成功或者失败)调用此方法,将请求的结果传递给z-paging作分页处理(若调用了此方法,则上拉加载更多时内部会自动分页,不会触发@query所绑定的事件)
        setLocalPaging(data, success = true) {
            this.isLocalPaging = true;
            this.$nextTick(() => {
                this._addData(data, success, true);
            })
            return new Promise((resolve, reject) => {
                this.dataPromiseResultMap.localPaging = { resolve, reject };
            });
        },
        // é‡æ–°åŠ è½½åˆ†é¡µæ•°æ®ï¼ŒpageNo会恢复为默认值,相当于下拉刷新的效果(animate为true时会展示下拉刷新动画,默认为false)
        reload(animate = this.showRefresherWhenReload) {
            if (animate) {
                this.privateShowRefresherWhenReload = animate;
                this.isUserPullDown = true;
            }
            if (!this.showLoadingMoreWhenReload) {
                this.listRendering = true;
            }
            this.$nextTick(() => {
                this._preReload(animate, false);
            })
            return new Promise((resolve, reject) => {
                this.dataPromiseResultMap.reload = { resolve, reject };
            });
        },
        // åˆ·æ–°åˆ—表数据,pageNo和pageSize不会重置,列表数据会重新从服务端获取。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
        refresh() {
            return this._handleRefreshWithDisPageNo(this.pageNo - this.defaultPageNo + 1);
        },
        // åˆ·æ–°åˆ—表数据至指定页,例如pageNo=5时则代表刷新列表至第5页,此时pageNo会变为5,列表会展示前5页的数据。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
        refreshToPage(pageNo) {
            this.isHandlingRefreshToPage = true;
            return this._handleRefreshWithDisPageNo(pageNo + this.defaultPageNo - 1);
        },
        // æ‰‹åŠ¨æ›´æ–°åˆ—è¡¨ç¼“å­˜æ•°æ®ï¼Œå°†è‡ªåŠ¨æˆªå–v-model绑定的list中的前pageSize条覆盖缓存,请确保在list数据更新到预期结果后再调用此方法
        updateCache() {
            if (this.finalUseCache && this.totalData.length) {
                this._saveLocalCache(this.totalData.slice(0, Math.min(this.totalData.length, this.pageSize)));
            }
        },
        // æ¸…空分页数据
        clean() {
            this._reload(true);
            this._addData([], true, false);
        },
        // æ¸…空分页数据
        clear() {
            this.clean();
        },
        // reload之前的一些处理
        _preReload(animate = this.showRefresherWhenReload, isFromMounted = true, retryCount = 0) {
            const showRefresher = this.finalRefresherEnabled && this.useCustomRefresher;
            // #ifndef APP-NVUE
            // å¦‚果获取slot="refresher"高度失败,则不触发reload,直到获取slot="refresher"高度成功
            if (this.customRefresherHeight === -1 && showRefresher) {
                u.delay(() => {
                    retryCount ++;
                    // å¦‚果重试次数是10的倍数(也就是每500毫秒),尝试重新获取一下slot="refresher"高度
                    // æ­¤ä¸¾æ˜¯ä¸ºäº†è§£å†³åœ¨æŸäº›ç‰¹æ®Šæƒ…况下,z-paging组件mounted了,但是未展示在用户面前,(比如在tabbar页面中,未切换到对应tabbar但是通过代码让z-paging展示了,此时控制台会报Error: Not Found:Page,因为这时候去获取dom节点信息获取不到)
                    // å½“用户在某个时刻让此z-paging展示在面前时,即可顺利获取到slot="refresher"高度,递归停止
                    if (retryCount % 10 === 0) {
                        this._updateCustomRefresherHeight();
                    }
                    this._preReload(animate, isFromMounted, retryCount);
                }, c.delayTime / 2);
                return;
            }
            // #endif
            this.isUserReload = true;
            this.loadingType = Enum.LoadingType.Refresher;
            if (animate) {
                this.privateShowRefresherWhenReload = animate;
                // #ifndef APP-NVUE
                if (this.useCustomRefresher) {
                    this._doRefresherRefreshAnimate();
                } else {
                    this.refresherTriggered = true;
                }
                // #endif
                // #ifdef APP-NVUE
                this.refresherStatus = Enum.Refresher.Loading;
                this.refresherRevealStackCount ++;
                u.delay(() => {
                    this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
                        if (node) {
                            let nodeHeight = node[0].height;
                            this.nShowRefresherReveal = true;
                            this.nShowRefresherRevealHeight = nodeHeight;
                            u.delay(() => {
                                this._nDoRefresherEndAnimation(0, -nodeHeight, false, false);
                                u.delay(() => {
                                    this._nDoRefresherEndAnimation(nodeHeight, 0);
                                }, 10)
                            }, 10)
                        }
                        this._reload(false, isFromMounted);
                        this._doRefresherLoad(false);
                    });
                }, this.pagingLoaded ? 10 : 100)
                return;
                // #endif
            } else {
                this._refresherEnd(false, false, false, false);
            }
            this._reload(false, isFromMounted);
        },
        // é‡æ–°åŠ è½½åˆ†é¡µæ•°æ®
        _reload(isClean = false, isFromMounted = false, isUserPullDown = false) {
            this.isAddedData = false;
            this.insideOfPaging = -1;
            this.cacheScrollNodeHeight = -1;
            this.pageNo = this.defaultPageNo;
            this._cleanRefresherEndTimeout();
            !this.privateShowRefresherWhenReload && !isClean && this._startLoading(true);
            this.firstPageLoaded = true;
            this.isTotalChangeFromAddData = false;
            if (!this.isSettingCacheList) {
                this.totalData = [];
            }
            if (!isClean) {
                this._emitQuery(this.pageNo, this.defaultPageSize, isUserPullDown ? Enum.QueryFrom.UserPullDown : Enum.QueryFrom.Reload);
                let delay = 0;
                // #ifdef MP-TOUTIAO
                delay = 5;
                // #endif
                u.delay(this._callMyParentQuery, delay);
                if (!isFromMounted && this.autoScrollToTopWhenReload) {
                    let checkedNRefresherLoading = true;
                    // #ifdef APP-NVUE
                    checkedNRefresherLoading = !this.nRefresherLoading;
                    // #endif
                    checkedNRefresherLoading && this._scrollToTop(false);
                }
            }
            // #ifdef APP-NVUE
            this.$nextTick(() => {
                this.nShowBottom = this.realTotalData.length > 0;
            })
            // #endif
        },
        // å¤„理服务端返回的数组
        _addData(data, success, isLocal) {
            this.isAddedData = true;
            this.fromEmptyViewReload = false;
            this.isTotalChangeFromAddData = true;
            this.refresherTriggered = false;
            this._endSystemLoadingAndRefresh();
            const tempIsUserPullDown = this.isUserPullDown;
            if (this.showRefresherUpdateTime && this.isFirstPage) {
                u.setRefesrherTime(u.getTime(), this.refresherUpdateTimeKey);
                this.$refs.refresh && this.$refs.refresh.updateTime();
            }
            if (!isLocal && tempIsUserPullDown && this.isFirstPage) {
                this.isUserPullDown = false;
            }
            this.listRendering = true;
            this.$nextTick(() => {
                u.delay(() => this.listRendering = false);
            })
            let dataTypeRes = this._checkDataType(data, success, isLocal);
            data = dataTypeRes.data;
            success = dataTypeRes.success;
            let delayTime = c.delayTime;
            if (this.useChatRecordMode) delayTime = 0;
            this.loadingForNow = false;
            u.delay(() => {
                this.pagingLoaded = true;
                this.$nextTick(()=>{
                    !isLocal && this._refresherEnd(delayTime > 0, true, tempIsUserPullDown);
                })
            })
            if (this.isFirstPage) {
                this.isLoadFailed = !success;
                this.$emit('isLoadFailedChange', this.isLoadFailed);
                if (this.finalUseCache && success && (this.cacheMode === Enum.CacheMode.Always ? true : this.isSettingCacheList)) {
                    this._saveLocalCache(data);
                }
            }
            this.isSettingCacheList = false;
            if (success) {
                if (!(this.privateConcat === false && !this.isHandlingRefreshToPage && this.loadingStatus === Enum.More.NoMore)) {
                    this.loadingStatus = Enum.More.Default;
                }
                if (isLocal) {
                    // å¦‚果当前是本地分页,则必然是由setLocalPaging方法触发,此时直接本地加载第一页数据即可。后续本地分页加载更多方法由滚动到底部加载更多事件处理
                    this.totalLocalPagingList = data;
                    const localPageNo = this.defaultPageNo;
                    const localPageSize = this.queryFrom !== Enum.QueryFrom.Refresh ? this.defaultPageSize : this.currentRefreshPageSize;
                    this._localPagingQueryList(localPageNo, localPageSize, 0, res => {
                        u.delay(() => {
                            this.completeByTotal(res, this.totalLocalPagingList.length);;
                        }, 0)
                    })
                } else {
                    // å¦‚果当前不是本地分页,则按照正常分页逻辑进行数据处理&emit数据
                    let dataChangeDelayTime = 0;
                    // #ifdef APP-NVUE
                    if (this.privateShowRefresherWhenReload && this.finalNvueListIs === 'waterfall') {
                        dataChangeDelayTime = 150;
                    }
                    // #endif
                    u.delay(() => {
                        this._currentDataChange(data, this.currentData);
                        this._callDataPromise(true, this.totalData);
                    }, dataChangeDelayTime)
                }
                if (this.isHandlingRefreshToPage) {
                    this.isHandlingRefreshToPage = false;
                    this.pageNo = this.defaultPageNo + Math.ceil(data.length / this.pageSize) - 1;
                    if (data.length % this.pageSize !== 0) {
                        this.customNoMore = 1;
                    }
                }
            } else {
                this._currentDataChange(data, this.currentData);
                this._callDataPromise(false);
                this.loadingStatus = Enum.More.Fail;
                this.isHandlingRefreshToPage = false;
                if (this.loadingType === Enum.LoadingType.LoadMore) {
                    this.pageNo --;
                }
            }
        },
        // æ‰€æœ‰æ•°æ®æ”¹å˜æ—¶è°ƒç”¨
        _totalDataChange(newVal, oldVal, eventThrow=true) {
            if ((!this.isUserReload || !this.autoCleanListWhenReload) && this.firstPageLoaded && !newVal.length && oldVal.length) {
                return;
            }
            this._doCheckScrollViewShouldFullHeight(newVal);
            if(!this.realTotalData.length && !newVal.length){
                eventThrow = false;
            }
            this.realTotalData = newVal;
            // emit列表更新事件
            if (eventThrow) {
                this.$emit('input', newVal);
                // #ifdef VUE3
                this.$emit('update:modelValue', newVal);
                // #endif
                this.$emit('update:list', newVal);
                this.$emit('listChange', newVal);
                this._callMyParentList(newVal);
            }
            this.firstPageLoaded = false;
            this.isTotalChangeFromAddData = false;
            this.$nextTick(() => {
                u.delay(()=>{
                    // emit z-paging内容区域高度改变事件
                    this._getNodeClientRect('.zp-paging-container-content').then(res => {
                        res && this.$emit('contentHeightChanged', res[0].height);
                    });
                }, c.delayTime * (this.isIos ? 1 : 3))
                // #ifdef APP-NVUE
                // åœ¨nvue中延时600毫秒展示底部加载更多,避免底部加载更多太早加载闪一下的问题
                u.delay(() => {
                    this.nShowBottom = true;
                }, c.delayTime * 6, 'nShowBottomDelay');
                // #endif
            })
        },
        // å½“前数据改变时调用
        _currentDataChange(newVal, oldVal) {
            newVal = [...newVal];
            // #ifndef APP-NVUE
            this.finalUseVirtualList && this._setCellIndex(newVal, 'bottom');
            // #endif
            if (this.isFirstPage && this.finalConcat) {
                this.totalData = [];
            }
            // customNoMore:-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
            if (this.customNoMore !== -1) {
                // å¦‚æžœcustomNoMore等于1 æˆ–者 customNoMore不是0并且新增数组长度为0(也就是不是明确的还有更多数据并且新增的数组长度为0),则没有更多数据了
                if (this.customNoMore === 1 || (this.customNoMore !== 0 && !newVal.length)) {
                    this.loadingStatus = Enum.More.NoMore;
                }
            } else {
                // å¦‚果新增的数据数组长度为0 æˆ–者 æ–°å¢žçš„æ•°ç»„长度小于默认的pageSize,则没有更多数据了
                if (!newVal.length || (newVal.length && newVal.length < this.defaultPageSize)) {
                    this.loadingStatus = Enum.More.NoMore;
                }
            }
            if (!this.totalData.length) {
                // #ifdef APP-NVUE
                // å¦‚果在聊天记录模式+nvue中,并且数据不满一页时需要将列表倒序,因为此时没有将列表旋转180度,数组中第0条数据应当在最底下显示
                if (this.useChatRecordMode && this.finalConcat && this.isFirstPage && this.loadingStatus === Enum.More.NoMore) {
                    newVal.reverse();
                }
                // #endif
                this.totalData = newVal;
            } else {
                if (this.finalConcat) {
                    const currentScrollTop = this.oldScrollTop;
                    this.totalData = [...this.totalData, ...newVal];
                    // æ­¤å¤„是为了解决在微信小程序中,在某些情况下滚动到底部加载更多后滚动位置直接变为最底部的问题,因此需要通过代码强制滚动回加载更多前的位置
                    // #ifdef MP-WEIXIN
                    if (!this.isIos && !this.refresherOnly && !this.usePageScroll && newVal.length) {
                        this.loadingMoreTimeStamp = u.getTime();
                        this.$nextTick(() => {
                            this.scrollToY(currentScrollTop);
                        })
                    }
                    // #endif
                } else {
                    this.totalData = newVal;
                }
            }
            this.privateConcat = true;
        },
        // æ ¹æ®pageNo处理refresh操作
        _handleRefreshWithDisPageNo(pageNo) {
            if (!this.isHandlingRefreshToPage && !this.realTotalData.length) return this.reload();
            if (pageNo >= 1) {
                this.loading = true;
                this.privateConcat = false;
                const totalPageSize = pageNo * this.pageSize;
                this.currentRefreshPageSize = totalPageSize;
                // å¦‚果调用refresh时是本地分页,则在组件内部自己处理分页逻辑,不emit query相关事件
                if (this.isLocalPaging && this.isHandlingRefreshToPage) {
                    this._localPagingQueryList(this.defaultPageNo, totalPageSize, 0, res => {
                        this.complete(res);
                    })
                } else {
                    // emit query相关事件
                    this._emitQuery(this.defaultPageNo, totalPageSize, Enum.QueryFrom.Refresh);
                    this._callMyParentQuery(this.defaultPageNo, totalPageSize);
                }
            }
            return new Promise((resolve, reject) => {
                this.dataPromiseResultMap.reload = { resolve, reject };
            });
        },
        // æœ¬åœ°åˆ†é¡µè¯·æ±‚
        _localPagingQueryList(pageNo, pageSize, localPagingLoadingTime, callback) {
            pageNo = Math.max(1, pageNo);
            pageSize = Math.max(1, pageSize);
            const totalPagingList = [...this.totalLocalPagingList];
            const pageNoIndex = (pageNo - 1) * pageSize;
            const finalPageNoIndex = Math.min(totalPagingList.length, pageNoIndex + pageSize);
            const resultPagingList = totalPagingList.splice(pageNoIndex, finalPageNoIndex - pageNoIndex);
            u.delay(() => callback(resultPagingList), localPagingLoadingTime)
        },
        // å­˜å‚¨åˆ—表缓存数据
        _saveLocalCache(data) {
            uni.setStorageSync(this.finalCacheKey, data);
        },
        // é€šè¿‡ç¼“存数据填充列表数据
        _setListByLocalCache() {
            this.totalData = uni.getStorageSync(this.finalCacheKey) || [];
            this.isSettingCacheList = true;
        },
        // ä¿®æ”¹çˆ¶view的list
        _callMyParentList(newVal) {
            if (this.autowireListName.length) {
                const myParent = u.getParent(this.$parent);
                if (myParent && myParent[this.autowireListName]) {
                    myParent[this.autowireListName] = newVal;
                }
            }
        },
        // è°ƒç”¨çˆ¶view的query
        _callMyParentQuery(customPageNo = 0, customPageSize = 0) {
            if (this.autowireQueryName) {
                if (this.myParentQuery === -1) {
                    const myParent = u.getParent(this.$parent);
                    if (myParent && myParent[this.autowireQueryName]) {
                        this.myParentQuery = myParent[this.autowireQueryName];
                    }
                }
                if (this.myParentQuery !== -1) {
                    customPageSize > 0 ? this.myParentQuery(customPageNo, customPageSize) : this.myParentQuery(this.pageNo, this.defaultPageSize);
                }
            }
        },
        // emit query事件
        _emitQuery(pageNo, pageSize, from){
            this.queryFrom = from;
            this.requestTimeStamp = u.getTime();
            const [lastItem] = this.realTotalData.slice(-1);
            if (this.fetch) {
                const fetchParams = interceptor._handleFetchParams({pageNo, pageSize, from, lastItem: lastItem || null}, this.fetchParams);
                const fetchResult = this.fetch(fetchParams);
                if (!interceptor._handleFetchResult(fetchResult, this, fetchParams)) {
                    u.isPromise(fetchResult) ? fetchResult.then(res => {
                        this.complete(res);
                    }).catch(err => {
                        this.complete(false);
                    }) : this.complete(fetchResult)
                }
            } else {
                this.$emit('query', ...interceptor._handleQuery(pageNo, pageSize, from, lastItem || null));
            }
        },
        // è§¦å‘数据改变promise
        _callDataPromise(success, totalList) {
            for (const key in this.dataPromiseResultMap) {
                const obj = this.dataPromiseResultMap[key];
                if (!obj) continue;
                success ? obj.resolve({ totalList, noMore: this.loadingStatus === Enum.More.NoMore }) : this.callNetworkReject && obj.reject(`z-paging-${key}-error`);
            }
        },
        // æ£€æŸ¥complete data的类型
        _checkDataType(data, success, isLocal) {
            const dataType = Object.prototype.toString.call(data);
            if (dataType === '[object Boolean]') {
                success = data;
                data = [];
            } else if (dataType !== '[object Array]') {
                data = [];
                if (dataType !== '[object Undefined]' && dataType !== '[object Null]') {
                    u.consoleErr(`${isLocal ? 'setLocalPaging' : 'complete'}参数类型不正确,第一个参数类型必须为Array!`);
                }
            }
            return { data, success };
        },
    }
}
src/components/z-paging/js/modules/empty.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
// [z-paging]空数据图view模块
import u from '.././z-paging-utils'
export default {
    props: {
        // æ˜¯å¦å¼ºåˆ¶éšè—ç©ºæ•°æ®å›¾ï¼Œé»˜è®¤ä¸ºå¦
        hideEmptyView: {
            type: Boolean,
            default: u.gc('hideEmptyView', false)
        },
        // ç©ºæ•°æ®å›¾æè¿°æ–‡å­—,默认为“没有数据哦~”
        emptyViewText: {
            type: [String, Object],
            default: u.gc('emptyViewText', null)
        },
        // æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®(无数据时),默认为否
        showEmptyViewReload: {
            type: Boolean,
            default: u.gc('showEmptyViewReload', false)
        },
        // åŠ è½½å¤±è´¥æ—¶æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®ï¼Œé»˜è®¤ä¸ºæ˜¯
        showEmptyViewReloadWhenError: {
            type: Boolean,
            default: u.gc('showEmptyViewReloadWhenError', true)
        },
        // ç©ºæ•°æ®å›¾ç‚¹å‡»é‡æ–°åŠ è½½æ–‡å­—ï¼Œé»˜è®¤ä¸ºâ€œé‡æ–°åŠ è½½â€
        emptyViewReloadText: {
            type: [String, Object],
            default: u.gc('emptyViewReloadText', null)
        },
        // ç©ºæ•°æ®å›¾å›¾ç‰‡ï¼Œé»˜è®¤ä½¿ç”¨z-paging内置的图片
        emptyViewImg: {
            type: String,
            default: u.gc('emptyViewImg', '')
        },
        // ç©ºæ•°æ®å›¾â€œåŠ è½½å¤±è´¥â€æè¿°æ–‡å­—ï¼Œé»˜è®¤ä¸ºâ€œå¾ˆæŠ±æ­‰ï¼ŒåŠ è½½å¤±è´¥â€
        emptyViewErrorText: {
            type: [String, Object],
            default: u.gc('emptyViewErrorText', null)
        },
        // ç©ºæ•°æ®å›¾â€œåŠ è½½å¤±è´¥â€å›¾ç‰‡ï¼Œé»˜è®¤ä½¿ç”¨z-paging内置的图片
        emptyViewErrorImg: {
            type: String,
            default: u.gc('emptyViewErrorImg', '')
        },
        // ç©ºæ•°æ®å›¾æ ·å¼
        emptyViewStyle: {
            type: Object,
            default: u.gc('emptyViewStyle', {})
        },
        // ç©ºæ•°æ®å›¾å®¹å™¨æ ·å¼
        emptyViewSuperStyle: {
            type: Object,
            default: u.gc('emptyViewSuperStyle', {})
        },
        // ç©ºæ•°æ®å›¾img样式
        emptyViewImgStyle: {
            type: Object,
            default: u.gc('emptyViewImgStyle', {})
        },
        // ç©ºæ•°æ®å›¾æè¿°æ–‡å­—样式
        emptyViewTitleStyle: {
            type: Object,
            default: u.gc('emptyViewTitleStyle', {})
        },
        // ç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®æ ·å¼
        emptyViewReloadStyle: {
            type: Object,
            default: u.gc('emptyViewReloadStyle', {})
        },
        // ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦é“ºæ»¡z-paging,默认为否,即填充满z-paging内列表(滚动区域)部分。若设置为否,则为填铺满整个z-paging
        emptyViewFixed: {
            type: Boolean,
            default: u.gc('emptyViewFixed', false)
        },
        // ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦åž‚直居中,默认为是,若设置为否即为从空数据容器顶部开始显示。emptyViewFixed为false时有效
        emptyViewCenter: {
            type: Boolean,
            default: u.gc('emptyViewCenter', true)
        },
        // åŠ è½½ä¸­æ—¶æ˜¯å¦è‡ªåŠ¨éšè—ç©ºæ•°æ®å›¾ï¼Œé»˜è®¤ä¸ºæ˜¯
        autoHideEmptyViewWhenLoading: {
            type: Boolean,
            default: u.gc('autoHideEmptyViewWhenLoading', true)
        },
        // ç”¨æˆ·ä¸‹æ‹‰åˆ—表触发下拉刷新加载中时是否自动隐藏空数据图,默认为是
        autoHideEmptyViewWhenPull: {
            type: Boolean,
            default: u.gc('autoHideEmptyViewWhenPull', true)
        },
        // ç©ºæ•°æ®view的z-index,默认为9
        emptyViewZIndex: {
            type: Number,
            default: u.gc('emptyViewZIndex', 9)
        },
    },
    data() {
        return {
            customerEmptyViewErrorText: ''
        }
    },
    computed: {
        finalEmptyViewImg() {
            return this.isLoadFailed ? this.emptyViewErrorImg : this.emptyViewImg;
        },
        finalShowEmptyViewReload() {
            return this.isLoadFailed ? this.showEmptyViewReloadWhenError : this.showEmptyViewReload;
        },
        // æ˜¯å¦å±•示空数据图
        showEmpty() {
            if (this.refresherOnly || this.hideEmptyView || this.realTotalData.length) return false;
            if (this.autoHideEmptyViewWhenLoading) {
                if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
            } else {
                return true;
            }
            return !this.autoHideEmptyViewWhenPull && !this.isUserReload;
        },
    },
    methods: {
        // ç‚¹å‡»äº†ç©ºæ•°æ®view重新加载按钮
        _emptyViewReload() {
            let callbacked = false;
            this.$emit('emptyViewReload', reload => {
                if (reload === undefined || reload === true) {
                    this.fromEmptyViewReload = true;
                    this.reload().catch(() => {});
                }
                callbacked = true;
            });
            // å¦‚果用户没有禁止默认的点击重新加载刷新列表事件,则触发列表重新刷新
            this.$nextTick(() => {
                if (!callbacked) {
                    this.fromEmptyViewReload = true;
                    this.reload().catch(() => {});
                }
            })
        },
        // ç‚¹å‡»äº†ç©ºæ•°æ®view
        _emptyViewClick() {
            this.$emit('emptyViewClick');
        },
    }
}
src/components/z-paging/js/modules/i18n.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
// [z-paging]i18n模块
import { initVueI18n } from '@dcloudio/uni-i18n'
import messages from '../../i18n/index.js'
const { t } = initVueI18n(messages)
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import interceptor from '../z-paging-interceptor'
export default {
    computed: {
        finalLanguage() {
            try {
                const local = uni.getLocale();
                const language = this.systemInfo.appLanguage;
                return local === 'auto' ? interceptor._handleLanguage2Local(language, this._language2Local(language)) : local;
            } catch (e) {
                // å¦‚果获取系统本地语言异常,则默认返回中文,uni.getLocale在部分低版本HX或者cli中可能报找不到的问题
                return 'zh-Hans';
            }
        },
        // æœ€ç»ˆçš„下拉刷新默认状态的文字
        finalRefresherDefaultText() {
            return this._getI18nText('zp.refresher.default', this.refresherDefaultText);
        },
        // æœ€ç»ˆçš„下拉刷新下拉中的文字
        finalRefresherPullingText() {
            return this._getI18nText('zp.refresher.pulling', this.refresherPullingText);
        },
        // æœ€ç»ˆçš„下拉刷新中文字
        finalRefresherRefreshingText() {
            return this._getI18nText('zp.refresher.refreshing', this.refresherRefreshingText);
        },
        // æœ€ç»ˆçš„下拉刷新完成文字
        finalRefresherCompleteText() {
            return this._getI18nText('zp.refresher.complete', this.refresherCompleteText);
        },
        // æœ€ç»ˆçš„下拉刷新上次更新时间文字
        finalRefresherUpdateTimeTextMap() {
            return {
                title: t('zp.refresherUpdateTime.title'),
                none: t('zp.refresherUpdateTime.none'),
                today: t('zp.refresherUpdateTime.today'),
                yesterday: t('zp.refresherUpdateTime.yesterday')
            };
        },
        // æœ€ç»ˆçš„继续下拉进入二楼文字
        finalRefresherGoF2Text() {
            return this._getI18nText('zp.refresher.f2', this.refresherGoF2Text);
        },
        // æœ€ç»ˆçš„底部加载更多默认状态文字
        finalLoadingMoreDefaultText() {
            return this._getI18nText('zp.loadingMore.default', this.loadingMoreDefaultText);
        },
        // æœ€ç»ˆçš„底部加载更多加载中文字
        finalLoadingMoreLoadingText() {
            return this._getI18nText('zp.loadingMore.loading', this.loadingMoreLoadingText);
        },
        // æœ€ç»ˆçš„底部加载更多没有更多数据文字
        finalLoadingMoreNoMoreText() {
            return this._getI18nText('zp.loadingMore.noMore', this.loadingMoreNoMoreText);
        },
        // æœ€ç»ˆçš„底部加载更多加载失败文字
        finalLoadingMoreFailText() {
            return this._getI18nText('zp.loadingMore.fail', this.loadingMoreFailText);
        },
        // æœ€ç»ˆçš„空数据图title
        finalEmptyViewText() {
            return this.isLoadFailed ? this.finalEmptyViewErrorText : this._getI18nText('zp.emptyView.title', this.emptyViewText);
        },
        // æœ€ç»ˆçš„空数据图reload title
        finalEmptyViewReloadText() {
            return this._getI18nText('zp.emptyView.reload', this.emptyViewReloadText);
        },
        // æœ€ç»ˆçš„空数据图加载失败文字
        finalEmptyViewErrorText() {
            return this.customerEmptyViewErrorText || this._getI18nText('zp.emptyView.error', this.emptyViewErrorText);
        },
        // æœ€ç»ˆçš„系统loading title
        finalSystemLoadingText() {
            return this._getI18nText('zp.systemLoading.title', this.systemLoadingText);
        },
    },
    methods: {
        // èŽ·å–å½“å‰z-paging的语言
        getLanguage() {
            return this.finalLanguage;
        },
        // èŽ·å–å›½é™…åŒ–è½¬æ¢åŽçš„æ–‡æœ¬
        _getI18nText(key, value) {
            const dataType = Object.prototype.toString.call(value);
            if (dataType === '[object Object]') {
                const nextValue = value[this.finalLanguage];
                if (nextValue) return nextValue;
            } else if (dataType === '[object String]') {
                return value;
            }
            return t(key);
        },
        // ç³»ç»Ÿlanguage转i18n local
        _language2Local(language) {
            const formatedLanguage = language.toLowerCase().replace(new RegExp('_', ''), '-');
            if (formatedLanguage.indexOf('zh') !== -1) {
                if (formatedLanguage === 'zh' || formatedLanguage === 'zh-cn' || formatedLanguage.indexOf('zh-hans') !== -1) {
                    return 'zh-Hans';
                }
                return 'zh-Hant';
            }
            if (formatedLanguage.indexOf('en') !== -1) return 'en';
            return language;
        }
    }
}
src/components/z-paging/js/modules/load-more.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,374 @@
// [z-paging]滚动到底部加载更多模块
import u from '.././z-paging-utils'
import Enum from '.././z-paging-enum'
export default {
    props: {
        // è‡ªå®šä¹‰åº•部加载更多样式
        loadingMoreCustomStyle: {
            type: Object,
            default: u.gc('loadingMoreCustomStyle', {})
        },
        // è‡ªå®šä¹‰åº•部加载更多文字样式
        loadingMoreTitleCustomStyle: {
            type: Object,
            default: u.gc('loadingMoreTitleCustomStyle', {})
        },
        // è‡ªå®šä¹‰åº•部加载更多加载中动画样式
        loadingMoreLoadingIconCustomStyle: {
            type: Object,
            default: u.gc('loadingMoreLoadingIconCustomStyle', {})
        },
        // è‡ªå®šä¹‰åº•部加载更多加载中动画图标类型,可选flower或circle,默认为flower
        loadingMoreLoadingIconType: {
            type: String,
            default: u.gc('loadingMoreLoadingIconType', 'flower')
        },
        // è‡ªå®šä¹‰åº•部加载更多加载中动画图标图片
        loadingMoreLoadingIconCustomImage: {
            type: String,
            default: u.gc('loadingMoreLoadingIconCustomImage', '')
        },
        // åº•部加载更多加载中view是否展示旋转动画,默认为是
        loadingMoreLoadingAnimated: {
            type: Boolean,
            default: u.gc('loadingMoreLoadingAnimated', true)
        },
        // æ˜¯å¦å¯ç”¨åŠ è½½æ›´å¤šæ•°æ®(含滑动到底部加载更多数据和点击加载更多数据),默认为是
        loadingMoreEnabled: {
            type: Boolean,
            default: u.gc('loadingMoreEnabled', true)
        },
        // æ˜¯å¦å¯ç”¨æ»‘动到底部加载更多数据,默认为是
        toBottomLoadingMoreEnabled: {
            type: Boolean,
            default: u.gc('toBottomLoadingMoreEnabled', true)
        },
        // æ»‘动到底部状态为默认状态时,以加载中的状态展示,默认为否。若设置为是,可避免滚动到底部看到默认状态然后立刻变为加载中状态的问题,但分页数量未超过一屏时,不会显示【点击加载更多】
        loadingMoreDefaultAsLoading: {
            type: Boolean,
            default: u.gc('loadingMoreDefaultAsLoading', false)
        },
        // æ»‘动到底部"默认"文字,默认为【点击加载更多】
        loadingMoreDefaultText: {
            type: [String, Object],
            default: u.gc('loadingMoreDefaultText', null)
        },
        // æ»‘动到底部"加载中"文字,默认为【正在加载...】
        loadingMoreLoadingText: {
            type: [String, Object],
            default: u.gc('loadingMoreLoadingText', null)
        },
        // æ»‘动到底部"没有更多"文字,默认为【没有更多了】
        loadingMoreNoMoreText: {
            type: [String, Object],
            default: u.gc('loadingMoreNoMoreText', null)
        },
        // æ»‘动到底部"加载失败"文字,默认为【加载失败,点击重新加载】
        loadingMoreFailText: {
            type: [String, Object],
            default: u.gc('loadingMoreFailText', null)
        },
        // å½“没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为否
        hideNoMoreInside: {
            type: Boolean,
            default: u.gc('hideNoMoreInside', false)
        },
        // å½“没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0,代表不限制。
        hideNoMoreByLimit: {
            type: Number,
            default: u.gc('hideNoMoreByLimit', 0)
        },
        // æ˜¯å¦æ˜¾ç¤ºé»˜è®¤çš„加载更多text,默认为是
        showDefaultLoadingMoreText: {
            type: Boolean,
            default: u.gc('showDefaultLoadingMoreText', true)
        },
        // æ˜¯å¦æ˜¾ç¤ºæ²¡æœ‰æ›´å¤šæ•°æ®çš„view
        showLoadingMoreNoMoreView: {
            type: Boolean,
            default: u.gc('showLoadingMoreNoMoreView', true)
        },
        // æ˜¯å¦æ˜¾ç¤ºæ²¡æœ‰æ›´å¤šæ•°æ®çš„分割线,默认为是
        showLoadingMoreNoMoreLine: {
            type: Boolean,
            default: u.gc('showLoadingMoreNoMoreLine', true)
        },
        // è‡ªå®šä¹‰åº•部没有更多数据的分割线样式
        loadingMoreNoMoreLineCustomStyle: {
            type: Object,
            default: u.gc('loadingMoreNoMoreLineCustomStyle', {})
        },
        // å½“分页未满一屏时,是否自动加载更多,默认为否(nvue无效)
        insideMore: {
            type: Boolean,
            default: u.gc('insideMore', false)
        },
        // è·åº•部/右边多远时(单位px),触发 scrolltolower äº‹ä»¶ï¼Œé»˜è®¤ä¸º100rpx
        lowerThreshold: {
            type: [Number, String],
            default: u.gc('lowerThreshold', '100rpx')
        },
    },
    data() {
        return {
            M: Enum.More,
            // åº•部加载更多状态
            loadingStatus: Enum.More.Default,
            // åœ¨æ¸²æŸ“之后的底部加载更多状态
            loadingStatusAfterRender: Enum.More.Default,
            // åº•部加载更多时间戳
            loadingMoreTimeStamp: 0,
            // åº•部加载更多slot
            loadingMoreDefaultSlot: null,
            // æ˜¯å¦å±•示底部加载更多
            showLoadingMore: false,
            // æ˜¯å¦æ˜¯å¼€å‘者自定义的加载更多,-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
            customNoMore: -1,
        }
    },
    computed: {
        // åº•部加载更多配置
        zLoadMoreConfig() {
            return {
                status: this.loadingStatusAfterRender,
                defaultAsLoading: this.loadingMoreDefaultAsLoading || (this.useChatRecordMode && this.chatLoadingMoreDefaultAsLoading),
                defaultThemeStyle: this.finalLoadingMoreThemeStyle,
                customStyle: this.loadingMoreCustomStyle,
                titleCustomStyle: this.loadingMoreTitleCustomStyle,
                iconCustomStyle: this.loadingMoreLoadingIconCustomStyle,
                loadingIconType: this.loadingMoreLoadingIconType,
                loadingIconCustomImage: this.loadingMoreLoadingIconCustomImage,
                loadingAnimated: this.loadingMoreLoadingAnimated,
                showNoMoreLine: this.showLoadingMoreNoMoreLine,
                noMoreLineCustomStyle: this.loadingMoreNoMoreLineCustomStyle,
                defaultText: this.finalLoadingMoreDefaultText,
                loadingText: this.finalLoadingMoreLoadingText,
                noMoreText: this.finalLoadingMoreNoMoreText,
                failText: this.finalLoadingMoreFailText,
                hideContent: !this.loadingMoreDefaultAsLoading && this.listRendering,
                unit: this.unit,
                isChat: this.useChatRecordMode,
                chatDefaultAsLoading: this.chatLoadingMoreDefaultAsLoading
            };
        },
        // æœ€ç»ˆçš„底部加载更多主题
        finalLoadingMoreThemeStyle() {
            return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
        },
        // æœ€ç»ˆçš„底部加载更多触发阈值
        finalLowerThreshold() {
            return u.convertToPx(this.lowerThreshold);
        },
        // æ˜¯å¦æ˜¾ç¤ºé»˜è®¤çŠ¶æ€ä¸‹çš„åº•éƒ¨åŠ è½½æ›´å¤š
        showLoadingMoreDefault() {
            return this._showLoadingMore('Default');
        },
        // æ˜¯å¦æ˜¾ç¤ºåŠ è½½ä¸­çŠ¶æ€ä¸‹çš„åº•éƒ¨åŠ è½½æ›´å¤š
        showLoadingMoreLoading() {
            return this._showLoadingMore('Loading');
        },
        // æ˜¯å¦æ˜¾ç¤ºæ²¡æœ‰æ›´å¤šäº†çŠ¶æ€ä¸‹çš„åº•éƒ¨åŠ è½½æ›´å¤š
        showLoadingMoreNoMore() {
            return this._showLoadingMore('NoMore');
        },
        // æ˜¯å¦æ˜¾ç¤ºåŠ è½½å¤±è´¥çŠ¶æ€ä¸‹çš„åº•éƒ¨åŠ è½½æ›´å¤š
        showLoadingMoreFail() {
            return this._showLoadingMore('Fail');
        },
        // æ˜¯å¦æ˜¾ç¤ºè‡ªå®šä¹‰çŠ¶æ€ä¸‹çš„åº•éƒ¨åŠ è½½æ›´å¤š
        showLoadingMoreCustom() {
            return this._showLoadingMore('Custom');
        },
        // åº•部加载更多固定高度
        loadingMoreFixedHeight() {
            return u.addUnit('80rpx', this.unit);
        },
    },
    methods: {
        // é¡µé¢æ»šåŠ¨åˆ°åº•éƒ¨æ—¶é€šçŸ¥z-paging进行进一步处理
        pageReachBottom() {
            !this.useChatRecordMode && this.toBottomLoadingMoreEnabled && this._onLoadingMore('toBottom');
        },
        // æ‰‹åŠ¨è§¦å‘ä¸Šæ‹‰åŠ è½½æ›´å¤š(非必须,可依据具体需求使用)
        doLoadMore(type) {
            this._onLoadingMore(type);
        },
        // é€šè¿‡@scroll事件检测是否滚动到了底部(顺带检测下是否滚动到了顶部)
        _checkScrolledToBottom(scrollDiff, checked = false) {
            // å¦‚果当前scroll-view高度未获取,则获取其高度
            if (this.cacheScrollNodeHeight === -1) {
                // èŽ·å–å½“å‰scroll-view高度
                this._getNodeClientRect('.zp-scroll-view').then((res) => {
                    if (res) {
                        const scrollNodeHeight = res[0].height;
                        // ç¼“存当前scroll-view高度,如果获取过了不再获取
                        this.cacheScrollNodeHeight = scrollNodeHeight;
                        // // scrollDiff - this.cacheScrollNodeHeight = å½“前滚动区域的顶部与内容底部的距离 - scroll-view高度 = å½“前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
                        if (scrollDiff - scrollNodeHeight <= this.finalLowerThreshold) {
                            // å¦‚果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
                            this._onLoadingMore('toBottom');
                        }
                    }
                });
            } else {
                // scrollDiff - this.cacheScrollNodeHeight = å½“前滚动区域的顶部与内容底部的距离 - scroll-view高度 = å½“前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
                if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
                    // å¦‚果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
                    this._onLoadingMore('toBottom');
                } else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
                    // å¦‚果与底部的距离小于500px,则获取当前滚动的位置,延迟150毫秒重复上述步骤再次检测(避免@scroll触发时获取的scrollTop不正确导致的其他问题,此时获取的scrollTop不一定可信)。防止因为部分性能较差安卓设备@scroll采样率过低导致的滚动到底部但是依然没有触发的问题
                    u.delay(() => {
                        this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
                            if (res) {
                                this.oldScrollTop = res[0].scrollTop;
                                const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
                                this._checkScrolledToBottom(newScrollDiff, true);
                            }
                        })
                    }, 150, 'checkScrolledToBottomDelay')
                }
                // æ£€æµ‹ä¸€ä¸‹æ˜¯å¦å·²ç»æ»šåŠ¨åˆ°äº†é¡¶éƒ¨äº†ï¼Œå› ä¸ºåœ¨å®‰å“ä¸­æ»šåŠ¨åˆ°é¡¶éƒ¨æ—¶scrollTop不一定为0(和滚动到底部一样的原因),所以需要在scrollTop小于150px时,通过获取.zp-scroll-view的scrollTop再判断一下
                if (this.oldScrollTop <= 150 && this.oldScrollTop !== 0) {
                    u.delay(() => {
                        // è¿™é‡Œå†åˆ¤æ–­ä¸€ä¸‹æ˜¯å¦ç¡®å®žå·²ç»æ»šåŠ¨åˆ°é¡¶éƒ¨äº†ï¼Œå¦‚æžœå·²ç»æ»šåŠ¨åˆ°é¡¶éƒ¨äº†ï¼Œåˆ™ä¸ç”¨å†åˆ¤æ–­äº†ï¼Œå†æ¬¡åˆ¤æ–­çš„åŽŸå› æ˜¯å¯èƒ½150毫秒之后oldScrollTop才是0
                        if (this.oldScrollTop !== 0) {
                            this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
                                // å¦‚æžœ150毫秒后.zp-scroll-view的scrollTop为0,则认为已经滚动到了顶部了
                                if (res && res[0].scrollTop === 0 && this.oldScrollTop !== 0) {
                                    this._onScrollToUpper();
                                }
                            })
                        }
                    }, 150, 'checkScrolledToTopDelay')
                }
            }
        },
        // è§¦å‘加载更多时调用,from:toBottom-滑动到底部触发;click-点击加载更多触发
        _onLoadingMore(from = 'click') {
            // å¦‚果是ios并且是滚动到底部的,则在滚动到底部时候尝试将列表设置为禁止滚动然后设置为允许滚动,以禁止底部bounce的效果
            if (this.isIos && from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
                this.scrollEnable = false;
                this.$nextTick(() => {
                    this.scrollEnable = true;
                })
            }
            // emit scrolltolower
            this._emitScrollEvent('scrolltolower');
            // å¦‚果是只使用下拉刷新 æˆ–者 ç¦ç”¨åº•部加载更多 æˆ–者 åº•部加载更多不是默认状态或加载失败状态 æˆ–者 æ˜¯åŠ è½½ä¸­çŠ¶æ€ æˆ–者 ç©ºæ•°æ®å›¾å·²ç»å±•示了,则return,不触发内部加载更多逻辑
            if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
            // #ifdef MP-WEIXIN
            if (!this.isIos && !this.refresherOnly && !this.usePageScroll) {
                const currentTimestamp = u.getTime();
                // åœ¨éžios平台+scroll-view中节流处理
                if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
                    this.loadingMoreTimeStamp = 0;
                    return;
                }
            }
            // #endif
            // å¤„理加载更多数据
            this._doLoadingMore();
        },
        // å¤„理开始加载更多
        _doLoadingMore() {
            if (this.pageNo >= this.defaultPageNo && this.loadingStatus !== Enum.More.NoMore) {
                this.pageNo ++;
                this._startLoading(false);
                if (this.isLocalPaging) {
                    // å¦‚果是本地分页,则在组件内部对数据进行分页处理,不触发@query事件
                    this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, res => {
                        this.completeByTotal(res, this.totalLocalPagingList.length);
                        this.queryFrom = Enum.QueryFrom.LoadMore;
                    })
                } else {
                    // emit @query相关加载更多事件
                    this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadMore);
                    this._callMyParentQuery();
                }
                // è®¾ç½®å½“前加载状态为底部加载更多状态
                this.loadingType = Enum.LoadingType.LoadMore;
            }
        },
        // (预处理)判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
        _preCheckShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode) {
            if (this.loadingStatus === Enum.More.NoMore && this.hideNoMoreByLimit > 0 && newVal.length) {
                this.showLoadingMore = newVal.length > this.hideNoMoreByLimit;
            } else if ((this.loadingStatus === Enum.More.NoMore && this.hideNoMoreInside && newVal.length) || (this.insideMore && this.insideOfPaging !== false && newVal.length)) {
                this.$nextTick(() => {
                    this._checkShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode);
                })
                if (this.insideMore && this.insideOfPaging !== false && newVal.length) {
                    this.showLoadingMore = newVal.length;
                }
            } else {
                this.showLoadingMore = newVal.length;
            }
        },
        // åˆ¤æ–­å½“没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
        async _checkShowNoMoreInside(totalData, oldScrollViewNode, oldPagingContainerNode) {
            try {
                const scrollViewNode = oldScrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
                // åœ¨é¡µé¢æ»šåŠ¨æ¨¡å¼ä¸‹
                if (this.usePageScroll) {
                    if (scrollViewNode) {
                        // èŽ·å–æ»šåŠ¨å†…å®¹æ€»é«˜åº¦
                        const scrollViewTotalH = scrollViewNode[0].top + scrollViewNode[0].height;
                        // å¦‚果滚动内容总高度小于窗口高度,则认为内容未超出z-paging
                        this.insideOfPaging = scrollViewTotalH < this.windowHeight;
                        // å¦‚果需要没有更多数据时,隐藏底部加载更多view,并且内容未超过z-paging,则隐藏底部加载更多
                        if (this.hideNoMoreInside) {
                            this.showLoadingMore = !this.insideOfPaging;
                        }
                        // å¦‚果需要内容未超过z-paging时自动加载更多,则触发加载更多
                        this._updateInsideOfPaging();
                    }
                } else {
                    // åœ¨scroll-view滚动模式下
                    const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
                    // èŽ·å–æ»šåŠ¨å†…å®¹æ€»é«˜åº¦
                    const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
                    // èŽ·å–z-paging内置scroll-view高度
                    const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
                    // å¦‚果滚动内容总高度小于z-paging内置scroll-view高度,则认为内容未超出z-paging
                    this.insideOfPaging = pagingContainerH < scrollViewH;
                    if (this.hideNoMoreInside) {
                        this.showLoadingMore = !this.insideOfPaging;
                    }
                    // å¦‚果需要内容未超过z-paging时自动加载更多,则触发加载更多
                    this._updateInsideOfPaging();
                }
            } catch (e) {
                // å¦‚果发生了异常,判断totalData数组长度为0,则认为内容未超出z-paging
                this.insideOfPaging = !totalData.length;
                if (this.hideNoMoreInside) {
                    this.showLoadingMore = !this.insideOfPaging;
                }
                // å¦‚果需要内容未超过z-paging时自动加载更多,则触发加载更多
                this._updateInsideOfPaging();
            }
        },
        // æ˜¯å¦è¦å±•示上拉加载更多view
        _showLoadingMore(type) {
            if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
            if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) ||
            (!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.refresherOnly) {
                return false;
            }
            if (this.useChatRecordMode && type !== 'Loading') return false;
            if (!this.zSlots) return false;
            if (type === 'Custom') {
                return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
            }
            const res = this.loadingStatus === Enum.More[type] && this.zSlots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
            if (res) {
                // #ifdef APP-NVUE
                if (!this.isIos) {
                    this.nLoadingMoreFixedHeight = false;
                }
                //  #endif
            }
            return res;
        },
    }
}
src/components/z-paging/js/modules/loading.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
// [z-paging]loading相关模块
import u from '.././z-paging-utils'
import Enum from '.././z-paging-enum'
export default {
    props: {
        // ç¬¬ä¸€æ¬¡åŠ è½½åŽè‡ªåŠ¨éšè—loading slot,默认为是
        autoHideLoadingAfterFirstLoaded: {
            type: Boolean,
            default: u.gc('autoHideLoadingAfterFirstLoaded', true)
        },
        // loading slot是否铺满屏幕并固定,默认为否
        loadingFullFixed: {
            type: Boolean,
            default: u.gc('loadingFullFixed', false)
        },
        // æ˜¯å¦è‡ªåŠ¨æ˜¾ç¤ºç³»ç»ŸLoading:即uni.showLoading,若开启则将在刷新列表时(调用reload、refresh时)显示,下拉刷新和滚动到底部加载更多不会显示,默认为false。
        autoShowSystemLoading: {
            type: Boolean,
            default: u.gc('autoShowSystemLoading', false)
        },
        // æ˜¾ç¤ºç³»ç»ŸLoading时是否显示透明蒙层,防止触摸穿透,默认为是(H5、App、微信小程序、百度小程序有效)
        systemLoadingMask: {
            type: Boolean,
            default: u.gc('systemLoadingMask', true)
        },
        // æ˜¾ç¤ºç³»ç»ŸLoading时显示的文字,默认为"加载中"
        systemLoadingText: {
            type: [String, Object],
            default: u.gc('systemLoadingText', null)
        },
    },
    data() {
        return {
            loading: false,
            loadingForNow: false,
        }
    },
    watch: {
        // loading状态
        loadingStatus(newVal) {
            this.$emit('loadingStatusChange', newVal);
            this.$nextTick(() => {
                this.loadingStatusAfterRender = newVal;
            })
            if (this.useChatRecordMode) {
                if (this.isFirstPage && (newVal === Enum.More.NoMore || newVal === Enum.More.Fail)) {
                    this.isFirstPageAndNoMore = true;
                    return;
                }
            }
            this.isFirstPageAndNoMore = false;
        },
        loading(newVal){
            if (newVal) {
                this.loadingForNow = newVal;
            }
        },
    },
    computed: {
        // æ˜¯å¦æ˜¾ç¤ºloading
        showLoading() {
            if (this.firstPageLoaded || !this.loading || !this.loadingForNow) return false;
            if (this.finalShowSystemLoading) {
                // æ˜¾ç¤ºç³»ç»Ÿloading
                uni.showLoading({
                    title: this.finalSystemLoadingText,
                    mask: this.systemLoadingMask
                })
            }
            return this.autoHideLoadingAfterFirstLoaded ? (this.fromEmptyViewReload ? true : !this.pagingLoaded) : this.loadingType === Enum.LoadingType.Refresher;
        },
        // æœ€ç»ˆçš„æ˜¯å¦æ˜¾ç¤ºç³»ç»Ÿloading
        finalShowSystemLoading() {
            return this.autoShowSystemLoading && this.loadingType === Enum.LoadingType.Refresher;
        }
    },
    methods: {
        // å¤„理开始加载更多状态
        _startLoading(isReload = false) {
            if ((this.showLoadingMoreWhenReload && !this.isUserPullDown) || !isReload) {
                this.loadingStatus = Enum.More.Loading;
            }
            this.loading = true;
        },
        // åœæ­¢ç³»ç»Ÿloading和refresh
        _endSystemLoadingAndRefresh(){
            this.finalShowSystemLoading && uni.hideLoading();
            !this.useCustomRefresher && uni.stopPullDownRefresh();
            // #ifdef APP-NVUE
            this.usePageScroll && uni.stopPullDownRefresh();
            // #endif
        }
    }
}
src/components/z-paging/js/modules/nvue.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
// [z-paging]nvue独有部分模块
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import Enum from '.././z-paging-enum'
// #ifdef APP-NVUE
const weexAnimation = weex.requireModule('animation');
// #endif
export default {
    props: {
        // #ifdef APP-NVUE
        // nvue中修改列表类型,可选值有list、waterfall和scroller,默认为list
        nvueListIs: {
            type: String,
            default: u.gc('nvueListIs', 'list')
        },
        // nvue waterfall配置,仅在nvue中且nvueListIs=waterfall时有效,配置参数详情参见:https://uniapp.dcloud.io/component/waterfall
        nvueWaterfallConfig: {
            type: Object,
            default: u.gc('nvueWaterfallConfig', {})
        },
        // nvue æŽ§åˆ¶æ˜¯å¦å›žå¼¹æ•ˆæžœï¼ŒiOS不支持动态修改
        nvueBounce: {
            type: Boolean,
            default: u.gc('nvueBounce', true)
        },
        // nvue中通过代码滚动到顶部/底部时,是否加快动画效果(无滚动动画时无效),默认为否
        nvueFastScroll: {
            type: Boolean,
            default: u.gc('nvueFastScroll', false)
        },
        // nvue中list的id
        nvueListId: {
            type: String,
            default: u.gc('nvueListId', '')
        },
        // nvue中refresh组件的样式
        nvueRefresherStyle: {
            type: Object,
            default: u.gc('nvueRefresherStyle', {})
        },
        // nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
        nvuePagingEnabled: {
            type: Boolean,
            default: u.gc('nvuePagingEnabled', false)
        },
        // æ˜¯å¦éšè—nvue列表底部的tagView,此view用于标识滚动到底部位置,若隐藏则滚动到底部功能将失效,在nvue中实现吸顶+swiper功能时需将最外层z-paging的此属性设置为true。默认为否
        hideNvueBottomTag: {
            type: Boolean,
            default: u.gc('hideNvueBottomTag', false)
        },
        // nvue中控制onscroll事件触发的频率:表示两次onscroll事件之间列表至少滚动了10px。注意,将该值设置为较小的数值会提高滚动事件采样的精度,但同时也会降低页面的性能
        offsetAccuracy: {
            type: Number,
            default: u.gc('offsetAccuracy', 10)
        },
        // #endif
    },
    data() {
        return {
            nRefresherLoading: false,
            nListIsDragging: false,
            nShowBottom: true,
            nFixFreezing: false,
            nShowRefresherReveal: false,
            nLoadingMoreFixedHeight: false,
            nShowRefresherRevealHeight: 0,
            nOldShowRefresherRevealHeight: -1,
            nRefresherWidth: u.rpx2px(750),
            nF2Opacity: 0
        }
    },
    computed: {
        // #ifdef APP-NVUE
        nScopedSlots() {
            // #ifdef VUE2
            return this.$scopedSlots;
            // #endif
            // #ifdef VUE3
            return null;
            // #endif
        },
        nWaterfallColumnCount() {
            if (this.finalNvueListIs !== 'waterfall') return 0;
            return this._nGetWaterfallConfig('column-count', 2);
        },
        nWaterfallColumnWidth() {
            return this._nGetWaterfallConfig('column-width', 'auto');
        },
        nWaterfallColumnGap() {
            return this._nGetWaterfallConfig('column-gap', 'normal');
        },
        nWaterfallLeftGap() {
            return this._nGetWaterfallConfig('left-gap', 0);
        },
        nWaterfallRightGap() {
            return this._nGetWaterfallConfig('right-gap', 0);
        },
        nViewIs() {
            const is = this.finalNvueListIs;
            return is === 'scroller' || is === 'view' ? 'view' : is === 'waterfall' ? 'header' : 'cell';
        },
        nSafeAreaBottomHeight() {
            return this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
        },
        finalNvueListIs() {
            if (this.usePageScroll) return 'view';
            const nvueListIsLowerCase = this.nvueListIs.toLowerCase();
            if (['list','waterfall','scroller'].indexOf(nvueListIsLowerCase) !== -1) return nvueListIsLowerCase;
            return 'list';
        },
        finalNvueSuperListIs() {
            return this.usePageScroll ? 'view' : 'scroller';
        },
        finalNvueRefresherEnabled() {
            return this.finalNvueListIs !== 'view' && this.finalRefresherEnabled && !this.nShowRefresherReveal && !this.useChatRecordMode;
        },
        // #endif
    },
    mounted(){
        // #ifdef APP-NVUE
        //旋转屏幕时更新宽度
        uni.onWindowResize((res) => {
            // this._nUpdateRefresherWidth();
        })
        // #endif
    },
    methods: {
        // #ifdef APP-NVUE
        // åˆ—表滚动时触发
        _nOnScroll(e) {
            this.$emit('scroll', e);
            const contentOffsetY = -e.contentOffset.y;
            this.oldScrollTop = contentOffsetY;
            this.nListIsDragging = e.isDragging;
            this._checkShouldShowBackToTop(contentOffsetY, contentOffsetY - 1);
        },
        // åˆ—表滚动结束
        _nOnScrollend(e) {
            this.$emit('scrollend', e);
            // åˆ¤æ–­æ˜¯å¦æ»šåŠ¨åˆ°é¡¶éƒ¨äº†
            if (e?.contentOffset?.y >= 0) {
                this._emitScrollEvent('scrolltoupper');
            }
            // åˆ¤æ–­æ˜¯å¦æ»šåŠ¨åˆ°åº•éƒ¨äº†
            this._getNodeClientRect('.zp-n-list').then(node => {
                if (node) {
                    if (e?.contentSize?.height + e?.contentOffset?.y <= node[0].height) {
                        this._emitScrollEvent('scrolltolower');
                    }
                }
            })
        },
        // ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­
        _nOnRrefresh() {
            if (this.nShowRefresherReveal) return;
            // è¿›å…¥åˆ·æ–°çŠ¶æ€
            this.nRefresherLoading = true;
            if (this.refresherStatus === Enum.Refresher.GoF2) {
                this._handleGoF2();
                this.$nextTick(() => {
                    this._nRefresherEnd();
                })
            } else {
                this.refresherStatus = Enum.Refresher.Loading;
                this._doRefresherLoad();
            }
        },
        // ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰ä¸­
        _nOnPullingdown(e) {
            if (this.refresherStatus === Enum.Refresher.Loading || (this.isIos && !this.nListIsDragging)) return;
            this._emitTouchmove(e);
            let { viewHeight, pullingDistance } = e;
            // æ›´æ–°ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€
            // ä¸‹æ‹‰åˆ·æ–°è·ç¦»è¶…过阈值
            if (pullingDistance >= viewHeight) {
                // å¦‚果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
                // (pullingDistance - viewHeight) + this.finalRefresherThreshold ä¸ç­‰åŒäºŽpullingDistance,此处是为了兼容不同平台下拉相同距离pullingDistance不一致的问题,pullingDistance仅与viewHeight互相关联
                this.refresherStatus = this.refresherF2Enabled && (pullingDistance - viewHeight) + this.finalRefresherThreshold >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
            } else {
                // ä¸‹æ‹‰åˆ·æ–°è·ç¦»æœªè¶…过阈值,显示默认状态
                this.refresherStatus = Enum.Refresher.Default;
            }
        },
        // ä¸‹æ‹‰åˆ·æ–°ç»“束
        _nRefresherEnd(doEnd = true) {
            if (doEnd) {
               this._nDoRefresherEndAnimation(0, -this.nShowRefresherRevealHeight);
               !this.usePageScroll && this.$refs['zp-n-list'].resetLoadmore();
               this.nRefresherLoading = false;
            }
        },
        // æ‰§è¡Œä¸»åŠ¨è§¦å‘ä¸‹æ‹‰åˆ·æ–°åŠ¨ç”»
        _nDoRefresherEndAnimation(height, translateY, animate = true, checkStack = true) {
            // æ¸…除下拉刷新相关timeout
            this._cleanRefresherCompleteTimeout();
            this._cleanRefresherEndTimeout();
            if (!this.finalShowRefresherWhenReload) {
                // å¦‚æžœreload不需要自动展示下拉刷新view,则在complete duration结束后再把下拉刷新状态设置回默认
                this.refresherEndTimeout = u.delay(() => {
                    this.refresherStatus = Enum.Refresher.Default;
                }, this.refresherCompleteDuration);
                return;
            }
            // ç”¨æˆ·å¤„理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
            const stackCount = this.refresherRevealStackCount;
            if (height === 0 && checkStack) {
                this.refresherRevealStackCount --;
                if (stackCount > 1) return;
                this.refresherEndTimeout = u.delay(() => {
                    this.refresherStatus = Enum.Refresher.Default;
                }, this.refresherCompleteDuration);
            }
            if (stackCount > 1) {
                this.refresherStatus = Enum.Refresher.Loading;
            }
            const duration = animate ? 200 : 0;
            if (this.nOldShowRefresherRevealHeight !== height) {
                if (height > 0) {
                    this.nShowRefresherReveal = true;
                }
                // å±•示下拉刷新view
                weexAnimation.transition(this.$refs['zp-n-list-refresher-reveal'], {
                    styles: {
                        height: `${height}px`,
                        transform: `translateY(${translateY}px)`,
                    },
                    duration,
                    timingFunction: 'linear',
                    needLayout: true,
                    delay: 0
                })
            }
            u.delay(() => {
                if (animate) {
                    this.nShowRefresherReveal = height > 0;
                }
            }, duration > 0 ? duration - 60 : 0);
            this.nOldShowRefresherRevealHeight = height;
        },
        // æ»šåŠ¨åˆ°åº•éƒ¨åŠ è½½æ›´å¤š
        _nOnLoadmore() {
            if (this.nShowRefresherReveal || !this.totalData.length) return;
            this.useChatRecordMode ? this.doChatRecordLoadMore() : this._onLoadingMore('toBottom');
        },
        // èŽ·å–nvue waterfall单项配置
        _nGetWaterfallConfig(key, defaultValue) {
            return this.nvueWaterfallConfig[key] || defaultValue;
        },
        // æ›´æ–°nvue ä¸‹æ‹‰åˆ·æ–°view容器的宽度
        _nUpdateRefresherWidth() {
            u.delay(() => {
                this.$nextTick(()=>{
                    this._getNodeClientRect('.zp-n-list').then(node => {
                        if (node) {
                            this.nRefresherWidth = node[0].width || this.nRefresherWidth;
                        }
                    })
                })
            })
        }
        // #endif
    }
}
src/components/z-paging/js/modules/refresher.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,831 @@
// [z-paging]下拉刷新view模块
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import Enum from '.././z-paging-enum'
// #ifdef APP-NVUE
const weexAnimation = weex.requireModule('animation');
// #endif
export default {
    props: {
        // ä¸‹æ‹‰åˆ·æ–°çš„主题样式,支持black,white,默认black
        refresherThemeStyle: {
            type: String,
            default: u.gc('refresherThemeStyle', '')
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å·¦ä¾§å›¾æ ‡çš„æ ·å¼
        refresherImgStyle: {
            type: Object,
            default: u.gc('refresherImgStyle', {})
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å³ä¾§çŠ¶æ€æè¿°æ–‡å­—çš„æ ·å¼
        refresherTitleStyle: {
            type: Object,
            default: u.gc('refresherTitleStyle', {})
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å³ä¾§æœ€åŽæ›´æ–°æ—¶é—´æ–‡å­—的样式(show-refresher-update-time为true时有效)
        refresherUpdateTimeStyle: {
            type: Object,
            default: u.gc('refresherUpdateTimeStyle', {})
        },
        // åœ¨å¾®ä¿¡å°ç¨‹åºå’ŒQQ小程序中,是否实时监听下拉刷新中进度,默认为否
        watchRefresherTouchmove: {
            type: Boolean,
            default: u.gc('watchRefresherTouchmove', false)
        },
        // åº•部加载更多的主题样式,支持black,white,默认black
        loadingMoreThemeStyle: {
            type: String,
            default: u.gc('loadingMoreThemeStyle', '')
        },
        // æ˜¯å¦åªä½¿ç”¨ä¸‹æ‹‰åˆ·æ–°ï¼Œè®¾ç½®ä¸ºtrue后将关闭mounted自动请求数据、关闭滚动到底部加载更多,强制隐藏空数据图。默认为否
        refresherOnly: {
            type: Boolean,
            default: u.gc('refresherOnly', false)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹å›žå¼¹åŠ¨ç”»æ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤ä¸º100毫秒,nvue无效
        refresherDefaultDuration: {
            type: [Number, String],
            default: u.gc('refresherDefaultDuration', 100)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束以后延迟回弹的时间,单位为毫秒,默认为0
        refresherCompleteDelay: {
            type: [Number, String],
            default: u.gc('refresherCompleteDelay', 0)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束回弹动画时间,单位为毫秒,默认为300毫秒(refresherEndBounceEnabled为false时,refresherCompleteDuration为设定值的1/3),nvue无效
        refresherCompleteDuration: {
            type: [Number, String],
            default: u.gc('refresherCompleteDuration', 300)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­æ˜¯å¦å…è®¸åˆ—表滚动,默认为是
        refresherRefreshingScrollable: {
            type: Boolean,
            default: u.gc('refresherRefreshingScrollable', true)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束状态下是否允许列表滚动,默认为否
        refresherCompleteScrollable: {
            type: Boolean,
            default: u.gc('refresherCompleteScrollable', false)
        },
        // æ˜¯å¦ä½¿ç”¨è‡ªå®šä¹‰çš„下拉刷新,默认为是,即使用z-paging的下拉刷新。设置为false即代表使用uni scroll-view自带的下拉刷新,h5、App、微信小程序以外的平台不支持uni scroll-view自带的下拉刷新
        useCustomRefresher: {
            type: Boolean,
            default: u.gc('useCustomRefresher', true)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰å¸§çŽ‡ï¼Œé»˜è®¤ä¸º40,过高可能会出现抖动问题
        refresherFps: {
            type: [Number, String],
            default: u.gc('refresherFps', 40)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°å…è®¸è§¦å‘的最大下拉角度,默认为40度,当下拉角度小于设定值时,自定义下拉刷新动画不会被触发
        refresherMaxAngle: {
            type: [Number, String],
            default: u.gc('refresherMaxAngle', 40)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°çš„角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为否
        refresherAngleEnableChangeContinued: {
            type: Boolean,
            default: u.gc('refresherAngleEnableChangeContinued', false)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹çš„æ–‡å­—
        refresherDefaultText: {
            type: [String, Object],
            default: u.gc('refresherDefaultText', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°æ¾æ‰‹ç«‹å³åˆ·æ–°çŠ¶æ€ä¸‹çš„æ–‡å­—
        refresherPullingText: {
            type: [String, Object],
            default: u.gc('refresherPullingText', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹çš„æ–‡å­—
        refresherRefreshingText: {
            type: [String, Object],
            default: u.gc('refresherRefreshingText', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束状态下的文字
        refresherCompleteText: {
            type: [String, Object],
            default: u.gc('refresherCompleteText', null)
        },
        // è‡ªå®šä¹‰ç»§ç»­ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼æ–‡å­—
        refresherGoF2Text: {
            type: [String, Object],
            default: u.gc('refresherGoF2Text', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹çš„å›¾ç‰‡
        refresherDefaultImg: {
            type: String,
            default: u.gc('refresherDefaultImg', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°æ¾æ‰‹ç«‹å³åˆ·æ–°çŠ¶æ€ä¸‹çš„å›¾ç‰‡ï¼Œé»˜è®¤ä¸ŽrefresherDefaultImg一致
        refresherPullingImg: {
            type: String,
            default: u.gc('refresherPullingImg', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹çš„å›¾ç‰‡
        refresherRefreshingImg: {
            type: String,
            default: u.gc('refresherRefreshingImg', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束状态下的图片
        refresherCompleteImg: {
            type: String,
            default: u.gc('refresherCompleteImg', null)
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹æ˜¯å¦å±•ç¤ºæ—‹è½¬åŠ¨ç”»
        refresherRefreshingAnimated: {
            type: Boolean,
            default: u.gc('refresherRefreshingAnimated', true)
        },
        // æ˜¯å¦å¼€å¯è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束回弹效果,默认为是
        refresherEndBounceEnabled: {
            type: Boolean,
            default: u.gc('refresherEndBounceEnabled', true)
        },
        // æ˜¯å¦å¼€å¯è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ï¼Œé»˜è®¤ä¸ºæ˜¯
        refresherEnabled: {
            type: Boolean,
            default: u.gc('refresherEnabled', true)
        },
        // è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é˜ˆå€¼ï¼Œé»˜è®¤ä¸º80rpx
        refresherThreshold: {
            type: [Number, String],
            default: u.gc('refresherThreshold', '80rpx')
        },
        // è®¾ç½®ç³»ç»Ÿä¸‹æ‹‰åˆ·æ–°é»˜è®¤æ ·å¼ï¼Œæ”¯æŒè®¾ç½® black,white,none,none è¡¨ç¤ºä¸ä½¿ç”¨é»˜è®¤æ ·å¼ï¼Œé»˜è®¤ä¸ºblack
        refresherDefaultStyle: {
            type: String,
            default: u.gc('refresherDefaultStyle', 'black')
        },
        // è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åŒºåŸŸèƒŒæ™¯
        refresherBackground: {
            type: String,
            default: u.gc('refresherBackground', 'transparent')
        },
        // è®¾ç½®å›ºå®šçš„自定义下拉刷新区域背景
        refresherFixedBackground: {
            type: String,
            default: u.gc('refresherFixedBackground', 'transparent')
        },
        // è®¾ç½®å›ºå®šçš„自定义下拉刷新区域高度,默认为0
        refresherFixedBacHeight: {
            type: [Number, String],
            default: u.gc('refresherFixedBacHeight', 0)
        },
        // è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰è¶…出阈值后继续下拉位移衰减的比例,范围0-1,值越大代表衰减越多。默认为0.65(nvue无效)
        refresherOutRate: {
            type: Number,
            default: u.gc('refresherOutRate', 0.65)
        },
        // æ˜¯å¦å¼€å¯ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼åŠŸèƒ½ï¼Œé»˜è®¤ä¸ºå¦
        refresherF2Enabled: {
            type: Boolean,
            default: u.gc('refresherF2Enabled', false)
        },
        // ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼é˜ˆå€¼ï¼Œé»˜è®¤ä¸º200rpx
        refresherF2Threshold: {
            type: [Number, String],
            default: u.gc('refresherF2Threshold', '200rpx')
        },
        // ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼åŠ¨ç”»æ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤ä¸º200毫秒
        refresherF2Duration: {
            type: [Number, String],
            default: u.gc('refresherF2Duration', 200)
        },
        // ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼çŠ¶æ€æ¾æ‰‹åŽæ˜¯å¦å¼¹å‡ºäºŒæ¥¼ï¼Œé»˜è®¤ä¸ºæ˜¯
        showRefresherF2: {
            type: Boolean,
            default: u.gc('showRefresherF2', true)
        },
        // è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰æ—¶å®žé™…下拉位移与用户下拉距离的比值,默认为0.75,即代表若用户下拉10px,则实际位移为7.5px(nvue无效)
        refresherPullRate: {
            type: Number,
            default: u.gc('refresherPullRate', 0.75)
        },
        // æ˜¯å¦æ˜¾ç¤ºæœ€åŽæ›´æ–°æ—¶é—´ï¼Œé»˜è®¤ä¸ºå¦
        showRefresherUpdateTime: {
            type: Boolean,
            default: u.gc('showRefresherUpdateTime', false)
        },
        // å¦‚果需要区别不同页面的最后更新时间,请为不同页面的z-paging的`refresher-update-time-key`设置不同的字符串
        refresherUpdateTimeKey: {
            type: String,
            default: u.gc('refresherUpdateTimeKey', 'default')
        },
        // ä¸‹æ‹‰åˆ·æ–°æ—¶ä¸‹æ‹‰åˆ°â€œæ¾æ‰‹ç«‹å³åˆ·æ–°â€æˆ–“松手进入二楼”状态时是否使手机短振动,默认为否(h5无效)
        refresherVibrate: {
            type: Boolean,
            default: u.gc('refresherVibrate', false)
        },
        // ä¸‹æ‹‰åˆ·æ–°æ—¶æ˜¯å¦ç¦æ­¢ä¸‹æ‹‰åˆ·æ–°view跟随用户触摸竖直移动,默认为否。注意此属性只是禁止下拉刷新view移动,其他下拉刷新逻辑依然会正常触发
        refresherNoTransform: {
            type: Boolean,
            default: u.gc('refresherNoTransform', false)
        },
        // æ˜¯å¦å¼€å¯ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ å ä½ï¼Œé€‚ç”¨äºŽéšè—å¯¼èˆªæ æ—¶ï¼Œä¸‹æ‹‰åˆ·æ–°éœ€è¦é¿å¼€çŠ¶æ€æ é«˜åº¦çš„æƒ…å†µï¼Œé»˜è®¤ä¸ºå¦
        useRefresherStatusBarPlaceholder: {
            type: Boolean,
            default: u.gc('useRefresherStatusBarPlaceholder', false)
        },
    },
    data() {
        return {
            R: Enum.Refresher,
            //下拉刷新状态
            refresherStatus: Enum.Refresher.Default,
            refresherTouchstartY: 0,
            lastRefresherTouchmove: null,
            refresherReachMaxAngle: true,
            refresherTransform: 'translateY(0px)',
            refresherTransition: '',
            finalRefresherDefaultStyle: 'black',
            refresherRevealStackCount: 0,
            refresherCompleteTimeout: null,
            refresherCompleteSubTimeout: null,
            refresherEndTimeout: null,
            isTouchmovingTimeout: null,
            refresherTriggered: false,
            isTouchmoving: false,
            isTouchEnded: false,
            isUserPullDown: false,
            privateRefresherEnabled: -1,
            privateShowRefresherWhenReload: false,
            customRefresherHeight: -1,
            showCustomRefresher: false,
            doRefreshAnimateAfter: false,
            isRefresherInComplete: false,
            showF2: false,
            f2Transform: '',
            pullDownTimeStamp: 0,
            moveDis: 0,
            oldMoveDis: 0,
            currentDis: 0,
            oldCurrentMoveDis: 0,
            oldRefresherTouchmoveY: 0,
            oldTouchDirection: '',
            oldEmitedTouchDirection: '',
            oldPullingDistance: -1,
            refresherThresholdUpdateTag: 0
        }
    },
    watch: {
        refresherDefaultStyle: {
            handler(newVal) {
                if (newVal.length) {
                    this.finalRefresherDefaultStyle = newVal;
                }
            },
            immediate: true
        },
        refresherStatus(newVal) {
            newVal === Enum.Refresher.Loading && this._cleanRefresherEndTimeout();
            this.refresherVibrate && (newVal === Enum.Refresher.ReleaseToRefresh || newVal === Enum.Refresher.GoF2) && this._doVibrateShort();
            this.$emit('refresherStatusChange', newVal);
            this.$emit('update:refresherStatus', newVal);
        },
        // ç›‘听当前下拉刷新启用/禁用状态
        refresherEnabled(newVal) {
            // å½“禁用下拉刷新时,强制收回正在展示的下拉刷新view
            !newVal && this.endRefresh();
        }
    },
    computed: {
        pullDownDisTimeStamp() {
            return 1000 / this.refresherFps;
        },
        refresherThresholdUnitConverted() {
            return u.addUnit(this.refresherThreshold, this.unit);
        },
        finalRefresherEnabled() {
            if (this.useChatRecordMode) return false;
            if (this.privateRefresherEnabled === -1) return this.refresherEnabled;
            return this.privateRefresherEnabled === 1;
        },
        finalRefresherThreshold() {
            let refresherThreshold = this.refresherThresholdUnitConverted;
            let idDefault = false;
            if (refresherThreshold === u.addUnit(80, this.unit)) {
                idDefault = true;
                if (this.showRefresherUpdateTime) {
                    refresherThreshold = u.addUnit(120, this.unit);
                }
            }
            if (idDefault && this.customRefresherHeight > 0) return this.customRefresherHeight + this.finalRefresherThresholdPlaceholder;
            return u.convertToPx(refresherThreshold) + this.finalRefresherThresholdPlaceholder;
        },
        finalRefresherF2Threshold() {
            return u.convertToPx(u.addUnit(this.refresherF2Threshold, this.unit));
        },
        finalRefresherThresholdPlaceholder() {
            return this.useRefresherStatusBarPlaceholder ? this.statusBarHeight : 0;
        },
        finalRefresherFixedBacHeight() {
            return u.convertToPx(this.refresherFixedBacHeight);
        },
        finalRefresherThemeStyle() {
            return this.refresherThemeStyle.length ? this.refresherThemeStyle : this.defaultThemeStyle;
        },
        finalRefresherOutRate() {
            let rate = this.refresherOutRate;
            rate = Math.max(0,rate);
            rate = Math.min(1,rate);
            return rate;
        },
        finalRefresherPullRate() {
            let rate = this.refresherPullRate;
            rate = Math.max(0,rate);
            return rate;
        },
        finalRefresherTransform() {
            if (this.refresherNoTransform || this.refresherTransform === 'translateY(0px)') return 'none';
            return this.refresherTransform;
        },
        finalShowRefresherWhenReload() {
            return this.showRefresherWhenReload || this.privateShowRefresherWhenReload;
        },
        finalRefresherTriggered() {
            if (!(this.finalRefresherEnabled && !this.useCustomRefresher)) return false;
            return this.refresherTriggered;
        },
        showRefresher() {
            const showRefresher = this.finalRefresherEnabled || this.useCustomRefresher && !this.useChatRecordMode;
            // #ifndef APP-NVUE
            this.active && this.customRefresherHeight === -1 && showRefresher && this.updateCustomRefresherHeight();
            // #endif
            return showRefresher;
        },
        hasTouchmove() {
            // #ifdef VUE2
            // #ifdef APP-VUE || H5
            if (this.$listeners && !this.$listeners.refresherTouchmove) return false;
            // #endif
            // #ifdef MP-WEIXIN || MP-QQ
            return this.watchRefresherTouchmove;
            // #endif
            return true;
            // #endif
            return this.watchRefresherTouchmove;
        },
    },
    methods: {
        // ç»ˆæ­¢ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€
        endRefresh() {
            this.totalData = this.realTotalData;
            this._refresherEnd();
            this._endSystemLoadingAndRefresh();
            this._handleScrollViewBounce({ bounce: true });
            this.$nextTick(() => {
                this.refresherTriggered = false;
            })
        },
        // æ‰‹åŠ¨æ›´æ–°è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°view高度
        updateCustomRefresherHeight() {
            u.delay(() => this.$nextTick(this._updateCustomRefresherHeight));
        },
        // å…³é—­äºŒæ¥¼
        closeF2() {
            this._handleCloseF2();
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°è¢«è§¦å‘
        _onRefresh(fromScrollView = false, isUserPullDown = true) {
            if (fromScrollView && !(this.finalRefresherEnabled && !this.useCustomRefresher)) return;
            this.$emit('onRefresh');
            this.$emit('Refresh');
            // #ifdef APP-NVUE
            if (this.loading) {
                u.delay(this._nRefresherEnd, 500)
                return;
            }
            // #endif
            if (this.loading || this.isRefresherInComplete) return;
            this.loadingType = Enum.LoadingType.Refresher;
            if (this.nShowRefresherReveal) return;
            this.isUserPullDown = isUserPullDown;
            this.isUserReload = !isUserPullDown;
            this._startLoading(true);
            this.refresherTriggered = true;
            if (this.reloadWhenRefresh && isUserPullDown) {
                this.useChatRecordMode ? this._onLoadingMore('click') : this._reload(false, false, isUserPullDown);
            }
        },
        // è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°è¢«å¤ä½
        _onRestore() {
            this.refresherTriggered = 'restore';
            this.$emit('onRestore');
            this.$emit('Restore');
        },
        // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
        // touch开始
        _refresherTouchstart(e) {
            this._handleListTouchstart();
            if (this._touchDisabled()) return;
            this._handleRefresherTouchstart(u.getTouch(e));
        },
        // #endif
        // è¿›ä¸€æ­¥å¤„理touch开始结果
        _handleRefresherTouchstart(touch) {
            if (!this.loading && this.isTouchEnded) {
                this.isTouchmoving = false;
            }
            this.loadingType = Enum.LoadingType.Refresher;
            this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
            this.isTouchEnded = false;
            this.refresherTransition = '';
            this.refresherTouchstartY = touch.touchY;
            this.$emit('refresherTouchstart', this.refresherTouchstartY);
            this.lastRefresherTouchmove = touch;
            this._cleanRefresherCompleteTimeout();
            this._cleanRefresherEndTimeout();
        },
        // éžapp-vue或微信小程序或QQ小程序或h5平台,使用js控制下拉刷新
        // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
        // touch中
        _refresherTouchmove(e) {
            const currentTimeStamp = u.getTime();
            let touch = null;
            let refresherTouchmoveY = 0;
            if (this.watchTouchDirectionChange) {
                // æ£€æµ‹ä¸‹æ‹‰åˆ·æ–°æ–¹å‘改变
                touch = u.getTouch(e);
                refresherTouchmoveY = touch.touchY;
                const direction  = refresherTouchmoveY > this.oldRefresherTouchmoveY ? 'top' : 'bottom';
                // åªæœ‰åœ¨æ–¹å‘改变的时候才emit相关事件
                if (direction === this.oldTouchDirection && direction !== this.oldEmitedTouchDirection) {
                    this._handleTouchDirectionChange({ direction });
                    this.oldEmitedTouchDirection = direction;
                }
                this.oldTouchDirection = direction;
                this.oldRefresherTouchmoveY = refresherTouchmoveY;
            }
            // èŠ‚æµå¤„ç†ï¼Œåœ¨pullDownDisTimeStamp时间内的下拉刷新中事件不进行处理
            if (this.pullDownTimeStamp && currentTimeStamp - this.pullDownTimeStamp <= this.pullDownDisTimeStamp) return;
            // å¦‚果不允许下拉,则return
            if (this._touchDisabled()) return;
            this.pullDownTimeStamp = Number(currentTimeStamp);
            touch = u.getTouch(e);
            refresherTouchmoveY = touch.touchY;
            // èŽ·å–å½“å‰touch的y - åˆå§‹touch的y,计算它们的差
            let moveDis = refresherTouchmoveY - this.refresherTouchstartY;
            if (moveDis < 0) return;
            // å¯¹ä¸‹æ‹‰åˆ·æ–°çš„角度进行限制
            if (this.refresherMaxAngle >= 0 && this.refresherMaxAngle <= 90 && this.lastRefresherTouchmove && this.lastRefresherTouchmove.touchY <= refresherTouchmoveY) {
                if (!moveDis && !this.refresherAngleEnableChangeContinued && this.moveDis < 1 && !this.refresherReachMaxAngle) return;
                const x = Math.abs(touch.touchX - this.lastRefresherTouchmove.touchX);
                const y = Math.abs(refresherTouchmoveY - this.lastRefresherTouchmove.touchY);
                const z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
                if ((x || y) && x > 1) {
                    // èŽ·å–ä¸‹æ‹‰åˆ·æ–°å‰åŽä¸¤æ¬¡ä½ç§»çš„è§’åº¦
                    const angle = Math.asin(y / z) / Math.PI * 180;
                    // å¦‚果角度小于配置要求,则return
                    if (angle < this.refresherMaxAngle) {
                        this.lastRefresherTouchmove = touch;
                        this.refresherReachMaxAngle = false;
                        return;
                    }
                }
            }
            // èŽ·å–æœ€ç»ˆçš„moveDis
            moveDis = this._getFinalRefresherMoveDis(moveDis);
            // å¤„理下拉刷新位移
            this._handleRefresherTouchmove(moveDis, touch);
            // ä¸‹æ‹‰åˆ·æ–°æ—¶ï¼Œç¦æ­¢é¡µé¢æ»šåŠ¨ä»¥é˜²æ­¢é¡µé¢å‘ä¸‹æ»šåŠ¨å’Œä¸‹æ‹‰åˆ·æ–°åŒæ—¶ä½œç”¨å¯¼è‡´ä¸‹æ‹‰åˆ·æ–°ä½ç½®åç§»è¶…è¿‡é¢„æœŸ
            if (!this.disabledBounce) {
                // #ifndef MP-LARK
                this._handleScrollViewBounce({ bounce: false });
                // #endif
                this.disabledBounce = true;
            }
            this._emitTouchmove({ pullingDistance: moveDis, dy: this.moveDis - this.oldMoveDis });
        },
        // #endif
        // è¿›ä¸€æ­¥å¤„理touch中结果
        _handleRefresherTouchmove(moveDis, touch) {
            this.refresherReachMaxAngle = true;
            this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
            this.isTouchmoving = true;
            this.isTouchEnded = false;
            // æ›´æ–°ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€
            // ä¸‹æ‹‰åˆ·æ–°è·ç¦»è¶…过阈值
            if (moveDis >= this.finalRefresherThreshold) {
                // å¦‚果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
                this.refresherStatus = this.refresherF2Enabled && moveDis >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
            } else {
                // ä¸‹æ‹‰åˆ·æ–°è·ç¦»æœªè¶…过阈值,显示默认状态
                this.refresherStatus = Enum.Refresher.Default;
            }
            // #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
            // this.scrollEnable = false;
            // é€šè¿‡transform控制下拉刷新view垂直偏移
            this.refresherTransform = `translateY(${moveDis}px)`;
            this.lastRefresherTouchmove = touch;
            // #endif
            this.moveDis = moveDis;
        },
        // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
        // touch结束
        _refresherTouchend(e) {
            // ä¸‹æ‹‰åˆ·æ–°ç”¨æˆ·æ‰‹ç¦»å¼€å±å¹•,允许列表滚动
            this._handleScrollViewBounce({bounce: true});
            if (this._touchDisabled() || !this.isTouchmoving) return;
            const touch = u.getTouch(e);
            let refresherTouchendY = touch.touchY;
            let moveDis = refresherTouchendY - this.refresherTouchstartY;
            moveDis = this._getFinalRefresherMoveDis(moveDis);
            this._handleRefresherTouchend(moveDis);
            this.disabledBounce = false;
        },
        // #endif
        // è¿›ä¸€æ­¥å¤„理touch结束结果
        _handleRefresherTouchend(moveDis) {
            // #ifndef APP-PLUS || H5 || MP-WEIXIN
            if (!this.isTouchmoving) return;
            // #endif
            this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
            this.refresherReachMaxAngle = true;
            this.isTouchEnded = true;
            const refresherThreshold = this.finalRefresherThreshold;
            if (moveDis >= refresherThreshold && (this.refresherStatus === Enum.Refresher.ReleaseToRefresh || this.refresherStatus === Enum.Refresher.GoF2)) {
                // å¦‚果是松手进入二楼状态,则触发进入二楼
                if (this.refresherStatus === Enum.Refresher.GoF2) {
                    this._handleGoF2();
                    this._refresherEnd();
                } else {
                    // å¦‚果是松手立即刷新状态,则触发下拉刷新
                    // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
                    this.refresherTransform = `translateY(${refresherThreshold}px)`;
                    this.refresherTransition = 'transform .1s linear';
                    // #endif
                    u.delay(() => {
                        this._emitTouchmove({ pullingDistance: refresherThreshold, dy: this.moveDis - refresherThreshold });
                    }, 0.1);
                    this.moveDis = refresherThreshold;
                    this.refresherStatus = Enum.Refresher.Loading;
                    this._doRefresherLoad();
                }
            } else {
                this._refresherEnd();
                this.isTouchmovingTimeout = u.delay(() => {
                    this.isTouchmoving = false;
                }, this.refresherDefaultDuration);
            }
            this.scrollEnable = true;
            this.$emit('refresherTouchend', moveDis);
        },
        // å¤„理列表触摸开始事件
        _handleListTouchstart() {
            if (this.useChatRecordMode && this.autoHideKeyboardWhenChat) {
                uni.hideKeyboard();
                this.$emit('hidedKeyboard');
            }
        },
        // å¤„理scroll-view bounce是否生效
        _handleScrollViewBounce({ bounce }) {
            if (!this.usePageScroll && !this.scrollToTopBounceEnabled) {
                if (this.wxsScrollTop <= 5) {
                    // #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
                    this.refresherTransition = '';
                    // #endif
                    this.scrollEnable = bounce;
                } else if (bounce) {
                    this.scrollEnable = bounce;
                }
            }
        },
        // wxs正在下拉状态改变处理
        _handleWxsPullingDownStatusChange(onPullingDown) {
            this.wxsOnPullingDown = onPullingDown;
            if (onPullingDown && !this.useChatRecordMode) {
                this.renderPropScrollTop = 0;
            }
        },
        // wxs正在下拉处理
        _handleWxsPullingDown({ moveDis, diffDis }){
            this._emitTouchmove({ pullingDistance: moveDis,dy: diffDis });
        },
        // wxs触摸方向改变
        _handleTouchDirectionChange({ direction }) {
            this.$emit('touchDirectionChange',direction);
        },
        // wxs通知更新其props
        _handlePropUpdate(){
            this.wxsPropType = u.getTime().toString();
        },
        // ä¸‹æ‹‰åˆ·æ–°ç»“束
        _refresherEnd(shouldEndLoadingDelay = true, fromAddData = false, isUserPullDown = false, setLoading = true) {
            if (this.loadingType === Enum.LoadingType.Refresher) {
                // è®¡ç®—当前下拉刷新结束需要延迟的时间
                const refresherCompleteDelay = (fromAddData && (isUserPullDown || this.showRefresherWhenReload)) ? this.refresherCompleteDelay : 0;
                // å¦‚果延迟时间大于0,则展示刷新结束状态,否则直接展示默认状态
                const refresherStatus = refresherCompleteDelay > 0 ? Enum.Refresher.Complete : Enum.Refresher.Default;
                if (this.finalShowRefresherWhenReload) {
                    const stackCount = this.refresherRevealStackCount;
                    this.refresherRevealStackCount --;
                    if (stackCount > 1) return;
                }
                this._cleanRefresherEndTimeout();
                this.refresherEndTimeout = u.delay(() => {
                    // æ›´æ–°ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€
                    this.refresherStatus = refresherStatus;
                    // å¦‚果当前下拉刷新状态不是刷新结束,则认为其不在刷新结束状态
                    if (refresherStatus !== Enum.Refresher.Complete) {
                        this.isRefresherInComplete = false;
                    }
                }, this.refresherStatus !== Enum.Refresher.Default && refresherStatus === Enum.Refresher.Default ? this.refresherCompleteDuration : 0);
                // #ifndef APP-NVUE
                if (refresherCompleteDelay > 0) {
                    this.isRefresherInComplete = true;
                }
                // #endif
                this._cleanRefresherCompleteTimeout();
                this.refresherCompleteTimeout = u.delay(() => {
                    let animateDuration = 1;
                    const animateType = this.refresherEndBounceEnabled && fromAddData ? 'cubic-bezier(0.19,1.64,0.42,0.72)' : 'linear';
                    if (fromAddData) {
                        animateDuration = this.refresherEndBounceEnabled ? this.refresherCompleteDuration / 1000 : this.refresherCompleteDuration / 3000;
                    }
                    this.refresherTransition = `transform ${fromAddData ? animateDuration : this.refresherDefaultDuration / 1000}s ${animateType}`;
                    // #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
                    this.refresherTransform = 'translateY(0px)';
                    this.currentDis = 0;
                    // #endif
                    // #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
                    this.wxsPropType = this.refresherTransition + 'end' + u.getTime();
                    // #endif
                    // #ifdef APP-NVUE
                    this._nRefresherEnd();
                    // #endif
                    this.moveDis = 0;
                    // #ifndef APP-NVUE
                    if (refresherStatus === Enum.Refresher.Complete) {
                        if (this.refresherCompleteSubTimeout) {
                            clearTimeout(this.refresherCompleteSubTimeout);
                            this.refresherCompleteSubTimeout = null;
                        }
                        this.refresherCompleteSubTimeout = u.delay(() => {
                            this.$nextTick(() => {
                                this.refresherStatus = Enum.Refresher.Default;
                                this.isRefresherInComplete = false;
                            })
                        }, animateDuration * 800);
                    }
                    // #endif
                    this._emitTouchmove({ pullingDistance: 0, dy: this.moveDis });
                }, refresherCompleteDelay);
            }
            if (setLoading) {
                u.delay(() => this.loading = false, shouldEndLoadingDelay ? 10 : 0);
                isUserPullDown && this._onRestore();
            }
        },
        // å¤„理进入二楼
        _handleGoF2() {
            if (this.showF2 || !this.refresherF2Enabled) return;
            this.$emit('refresherF2Change', 'go');
            if (!this.showRefresherF2) return;
            // #ifndef APP-NVUE
            this.f2Transform = `translateY(${-this.superContentHeight}px)`;
            this.showF2 = true;
            u.delay(() => {
                this.f2Transform = 'translateY(0px)';
            }, 100, 'f2ShowDelay')
            // #endif
            // #ifdef APP-NVUE
            this.showF2 = true;
            this.$nextTick(() => {
                weexAnimation.transition(this.$refs['zp-n-f2'], {
                    styles: { transform: `translateY(${-this.superContentHeight}px)` },
                    duration: 0,
                    timingFunction: 'linear',
                    needLayout: true,
                    delay: 0
                })
                this.nF2Opacity = 1;
            })
            u.delay(() => {
                weexAnimation.transition(this.$refs['zp-n-f2'], {
                    styles: { transform: 'translateY(0px)' },
                    duration: this.refresherF2Duration,
                    timingFunction: 'linear',
                    needLayout: true,
                    delay: 0
                })
            }, 10, 'f2GoDelay')
            // #endif
        },
        // å¤„理退出二楼
        _handleCloseF2() {
            if (!this.showF2 || !this.refresherF2Enabled) return;
            this.$emit('refresherF2Change', 'close');
            if (!this.showRefresherF2) return;
            // #ifndef APP-NVUE
            this.f2Transform = `translateY(${-this.superContentHeight}px)`;
            // #endif
            // #ifdef APP-NVUE
            weexAnimation.transition(this.$refs['zp-n-f2'], {
                styles: { transform: `translateY(${-this.superContentHeight}px)` },
                duration: this.refresherF2Duration,
                timingFunction: 'linear',
                needLayout: true,
                delay: 0
            })
            // #endif
            u.delay(() => {
                this.showF2 = false;
                this.nF2Opacity = 0;
            }, this.refresherF2Duration, 'f2CloseDelay')
        },
        // æ¨¡æ‹Ÿç”¨æˆ·æ‰‹åŠ¨è§¦å‘ä¸‹æ‹‰åˆ·æ–°
        _doRefresherRefreshAnimate() {
            this._cleanRefresherCompleteTimeout();
            // ç”¨æˆ·å¤„理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
            // #ifndef APP-NVUE
            const doRefreshAnimateAfter = !this.doRefreshAnimateAfter && (this.finalShowRefresherWhenReload) && this
                .customRefresherHeight === -1 && this.refresherThreshold === u.addUnit(80, this.unit);
            if (doRefreshAnimateAfter) {
                this.doRefreshAnimateAfter = true;
                return;
            }
            // #endif
            this.refresherRevealStackCount ++;
            // #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
            this.refresherTransform = `translateY(${this.finalRefresherThreshold}px)`;
            // #endif
            // #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
            this.wxsPropType = 'begin' + u.getTime();
            // #endif
            this.moveDis = this.finalRefresherThreshold;
            this.refresherStatus = Enum.Refresher.Loading;
            this.isTouchmoving = true;
            this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
            this._doRefresherLoad(false);
        },
        // è§¦å‘下拉刷新
        _doRefresherLoad(isUserPullDown = true) {
            this._onRefresh(false, isUserPullDown);
            this.loading = true;
        },
        // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
        // èŽ·å–å¤„ç†åŽçš„moveDis
        _getFinalRefresherMoveDis(moveDis) {
            let diffDis = moveDis - this.oldCurrentMoveDis;
            this.oldCurrentMoveDis = moveDis;
            if (diffDis > 0) {
                // æ ¹æ®é…ç½®çš„下拉刷新用户手势位移与实际需要的位移比率计算最终的diffDis
                diffDis = diffDis * this.finalRefresherPullRate;
                if (this.currentDis > this.finalRefresherThreshold) {
                    diffDis = diffDis * (1 - this.finalRefresherOutRate);
                }
            }
            // æŽ§åˆ¶diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
            diffDis = diffDis > 100 ? diffDis / 100 : diffDis;
            this.currentDis += diffDis;
            this.currentDis = Math.max(0, this.currentDis);
            return this.currentDis;
        },
        // åˆ¤æ–­touch手势是否要触发
        _touchDisabled() {
            const checkOldScrollTop = this.oldScrollTop > 5;
            return this.loading || this.isRefresherInComplete || this.useChatRecordMode || !this.refresherEnabled || !this.useCustomRefresher ||(this.usePageScroll && this.useCustomRefresher && this.pageScrollTop > 10) || (!(this.usePageScroll && this.useCustomRefresher) && checkOldScrollTop);
        },
        // #endif
        // æ›´æ–°è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°view高度
        _updateCustomRefresherHeight() {
            this._getNodeClientRect('.zp-custom-refresher-slot-view').then((res) => {
                this.customRefresherHeight = res ? res[0].height : 0;
                this.showCustomRefresher = this.customRefresherHeight > 0;
                if (this.doRefreshAnimateAfter) {
                    this.doRefreshAnimateAfter = false;
                    this._doRefresherRefreshAnimate();
                }
            });
        },
        // emit pullingDown事件
        _emitTouchmove(e) {
            // #ifndef APP-NVUE
            e.viewHeight = this.finalRefresherThreshold;
            // #endif
            e.rate = e.viewHeight > 0 ? e.pullingDistance / e.viewHeight : 0;
            this.hasTouchmove && this.oldPullingDistance !== e.pullingDistance && this.$emit('refresherTouchmove', e);
            this.oldPullingDistance = e.pullingDistance;
        },
        // æ¸…除refresherCompleteTimeout
        _cleanRefresherCompleteTimeout() {
            this.refresherCompleteTimeout = this._cleanTimeout(this.refresherCompleteTimeout);
            // #ifdef APP-NVUE
            this._nRefresherEnd(false);
            // #endif
        },
        // æ¸…除refresherEndTimeout
        _cleanRefresherEndTimeout() {
            this.refresherEndTimeout = this._cleanTimeout(this.refresherEndTimeout);
        },
    }
}
src/components/z-paging/js/modules/scroller.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,550 @@
// [z-paging]scroll相关模块
import u from '.././z-paging-utils'
import Enum from '.././z-paging-enum'
// #ifdef APP-NVUE
const weexDom = weex.requireModule('dom');
// #endif
export default {
    props: {
        // ä½¿ç”¨é¡µé¢æ»šåŠ¨ï¼Œé»˜è®¤ä¸ºå¦ï¼Œå½“è®¾ç½®ä¸ºæ˜¯æ—¶åˆ™ä½¿ç”¨é¡µé¢çš„æ»šåŠ¨è€Œéžæ­¤ç»„ä»¶å†…éƒ¨çš„scroll-view的滚动,使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高,但配置会略微繁琐
        usePageScroll: {
            type: Boolean,
            default: u.gc('usePageScroll', false)
        },
        // æ˜¯å¦å¯ä»¥æ»šåŠ¨ï¼Œä½¿ç”¨å†…ç½®scroll-view和nvue时有效,默认为是
        scrollable: {
            type: Boolean,
            default: u.gc('scrollable', true)
        },
        // æŽ§åˆ¶æ˜¯å¦å‡ºçŽ°æ»šåŠ¨æ¡ï¼Œé»˜è®¤ä¸ºæ˜¯
        showScrollbar: {
            type: Boolean,
            default: u.gc('showScrollbar', true)
        },
        // æ˜¯å¦å…è®¸æ¨ªå‘滚动,默认为否
        scrollX: {
            type: Boolean,
            default: u.gc('scrollX', false)
        },
        // iOS设备上滚动到顶部时是否允许回弹效果,默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯,但是有吸顶view时滚动到顶部时可能出现抖动。
        scrollToTopBounceEnabled: {
            type: Boolean,
            default: u.gc('scrollToTopBounceEnabled', false)
        },
        // iOS设备上滚动到底部时是否允许回弹效果,默认为是。
        scrollToBottomBounceEnabled: {
            type: Boolean,
            default: u.gc('scrollToBottomBounceEnabled', true)
        },
        // åœ¨è®¾ç½®æ»šåŠ¨æ¡ä½ç½®æ—¶ä½¿ç”¨åŠ¨ç”»è¿‡æ¸¡ï¼Œé»˜è®¤ä¸ºå¦
        scrollWithAnimation: {
            type: Boolean,
            default: u.gc('scrollWithAnimation', false)
        },
        // å€¼åº”为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
        scrollIntoView: {
            type: String,
            default: u.gc('scrollIntoView', '')
        },
    },
    data() {
        return {
            scrollTop: 0,
            oldScrollTop: 0,
            scrollLeft: 0,
            oldScrollLeft: 0,
            scrollViewStyle: {},
            scrollViewContainerStyle: {},
            scrollViewInStyle: {},
            pageScrollTop: -1,
            scrollEnable: true,
            privateScrollWithAnimation: -1,
            cacheScrollNodeHeight: -1,
            superContentHeight: 0,
        }
    },
    watch: {
        oldScrollTop(newVal) {
            !this.usePageScroll && this._scrollTopChange(newVal,false);
        },
        pageScrollTop(newVal) {
            this.usePageScroll && this._scrollTopChange(newVal,true);
        },
        usePageScroll: {
            handler(newVal) {
                this.loaded && this.autoHeight && this._setAutoHeight(!newVal);
                // #ifdef H5
                if (newVal) {
                    this.$nextTick(() => {
                        const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main;
                        if (mainScrollRef) {
                            mainScrollRef.style = {};
                        }
                    })
                }
                // #endif
            },
            immediate: true
        },
        finalScrollTop(newVal) {
            this.renderPropScrollTop = newVal < 6 ? 0 : 10;
        }
    },
    computed: {
        finalScrollWithAnimation() {
            if (this.privateScrollWithAnimation !== -1) {
                return this.privateScrollWithAnimation === 1;
            }
            return this.scrollWithAnimation;
        },
        finalScrollViewStyle() {
            if (this.superContentZIndex != 1) {
                this.scrollViewStyle['z-index'] = this.superContentZIndex;
                this.scrollViewStyle['position'] = 'relative';
            }
            return this.scrollViewStyle;
        },
        finalScrollTop() {
            return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop;
        },
        // å½“前是否是旧版webview
        finalIsOldWebView() {
            return this.isOldWebView && !this.usePageScroll;
        },
        // å½“前scroll-view/list-view是否允许滚动
        finalScrollable() {
            return this.scrollable && !this.usePageScroll && this.scrollEnable
            && (this.refresherCompleteScrollable ? true : this.refresherStatus !== Enum.Refresher.Complete)
            && (this.refresherRefreshingScrollable ? true : this.refresherStatus !== Enum.Refresher.Loading);
        }
    },
    methods: {
        // æ»šåŠ¨åˆ°é¡¶éƒ¨ï¼Œanimate为是否展示滚动动画,默认为是
        scrollToTop(animate, checkReverse = true) {
            // å¦‚果是聊天记录模式并且列表倒置了,则滚动到顶部实际上是滚动到底部
            if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
                this.scrollToBottom(animate, false);
                return;
            }
            this.$nextTick(() => {
                this._scrollToTop(animate, false);
                // #ifdef APP-NVUE
                if (this.nvueFastScroll && animate) {
                    u.delay(() => {
                        this._scrollToTop(false, false);
                    });
                }
                // #endif
            })
        },
        // æ»šåŠ¨åˆ°åº•éƒ¨ï¼Œanimate为是否展示滚动动画,默认为是
        scrollToBottom(animate, checkReverse = true) {
            // å¦‚果是聊天记录模式并且列表倒置了,则滚动到底部实际上是滚动到顶部
            if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
                this.scrollToTop(animate, false);
                return;
            }
            this.$nextTick(() => {
                this._scrollToBottom(animate);
                // #ifdef APP-NVUE
                if (this.nvueFastScroll && animate) {
                    u.delay(() => {
                        this._scrollToBottom(false);
                    });
                }
                // #endif
            })
        },
        // æ»šåŠ¨åˆ°æŒ‡å®šview(vue中有效)。sel为需要滚动的view的id值,不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollIntoViewById(sel, offset, animate) {
            this._scrollIntoView(sel, offset, animate);
        },
        // æ»šåŠ¨åˆ°æŒ‡å®šview(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollIntoViewByNodeTop(nodeTop, offset, animate) {
            this.scrollTop = this.oldScrollTop;
            this.$nextTick(() => {
                this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
            })
        },
        // y轴滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollToY(y, offset, animate) {
            this.scrollTop = this.oldScrollTop;
            this.$nextTick(() => {
                this._scrollToY(y, offset, animate);
            })
        },
        // x轴滚动到指定位置(非页面滚动且在vue中有效)。x为与左侧的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollToX(x, offset, animate) {
            this.scrollLeft = this.oldScrollLeft;
            this.$nextTick(() => {
                this._scrollToX(x, offset, animate);
            })
        },
        // æ»šåŠ¨åˆ°æŒ‡å®šview(nvue中和虚拟列表中有效)。index为需要滚动的view的index(第几个,从0开始);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollIntoViewByIndex(index, offset, animate) {
            if (index >= this.realTotalData.length) {
                u.consoleErr('当前滚动的index超出已渲染列表长度,请先通过refreshToPage加载到对应index页并等待渲染成功后再调用此方法!');
                return;
            }
            this.$nextTick(() => {
                // #ifdef APP-NVUE
                // åœ¨nvue中,根据index获取对应节点信息并滚动到此节点位置
                this._scrollIntoView(index, offset, animate);
                // #endif
                // #ifndef APP-NVUE
                if (this.finalUseVirtualList) {
                    const isCellFixed = this.cellHeightMode === Enum.CellHeightMode.Fixed;
                    u.delay(() => {
                        if (this.finalUseVirtualList) {
                            // è™šæ‹Ÿåˆ—表 + æ¯ä¸ªcell高度完全相同模式下,此时滚动到对应index的cell就是滚动到scrollTop = cellHeight * index的位置
                            // è™šæ‹Ÿåˆ—表 + é«˜åº¦æ˜¯åŠ¨æ€éžå›ºå®šçš„æ¨¡å¼ä¸‹ï¼Œæ­¤æ—¶æ»šåŠ¨åˆ°å¯¹åº”index的cell就是滚动到scrollTop = ç¼“存的cell高度数组中第index个的lastTotalHeight的位置
                            const scrollTop = isCellFixed ? this.virtualCellHeight * index : this.virtualHeightCacheList[index].lastTotalHeight;
                            this.scrollToY(scrollTop, offset, animate);
                        }
                    }, isCellFixed ? 0 : 100)
                }
                // #endif
            })
        },
        // æ»šåŠ¨åˆ°æŒ‡å®šview(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
        scrollIntoViewByView(view, offset, animate) {
            this._scrollIntoView(view, offset, animate);
        },
        // å½“使用页面滚动并且自定义下拉刷新时,请在页面的onPageScroll中调用此方法,告知z-paging当前的pageScrollTop,否则会导致在任意位置都可以下拉刷新
        updatePageScrollTop(value) {
            this.pageScrollTop = value;
        },
        // å½“使用页面滚动并且设置了slot="top"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="top"的view高度动态改变时,在其高度需要更新时调用此方法
        updatePageScrollTopHeight() {
            this._updatePageScrollTopOrBottomHeight('top');
        },
        // å½“使用页面滚动并且设置了slot="bottom"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="bottom"的view高度动态改变时,在其高度需要更新时调用此方法
        updatePageScrollBottomHeight() {
            this._updatePageScrollTopOrBottomHeight('bottom');
        },
        // æ›´æ–°slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
        updateLeftAndRightWidth() {
            if (!this.finalIsOldWebView) return;
            this.$nextTick(() => this._updateLeftAndRightWidth(this.scrollViewContainerStyle, 'zp-page'));
        },
        // æ›´æ–°z-paging内置scroll-view的scrollTop
        updateScrollViewScrollTop(scrollTop, animate = true) {
            this._updatePrivateScrollWithAnimation(animate);
            this.scrollTop = this.oldScrollTop;
            this.$nextTick(() => {
                this.scrollTop = scrollTop;
                this.oldScrollTop = this.scrollTop;
            });
        },
        // å½“滚动到顶部时
        _onScrollToUpper() {
            this._emitScrollEvent('scrolltoupper');
            this.$emit('scrollTopChange', 0);
            this.$nextTick(() => {
                this.oldScrollTop = 0;
            })
        },
        // å½“滚动到底部时
        _onScrollToLower(e) {
            (!e.detail || !e.detail.direction || e.detail.direction === 'bottom')
            && this.toBottomLoadingMoreEnabled
            && this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom')
        },
        // æ»šåŠ¨åˆ°é¡¶éƒ¨
        _scrollToTop(animate = true, isPrivate = true) {
            // #ifdef APP-NVUE
            // åœ¨nvue中需要通过weex.scrollToElement滚动到顶部,此时在顶部插入了一个view,使得滚动到这个view位置
            const el = this.$refs['zp-n-list-top-tag'];
            if (this.usePageScroll) {
                this._getNodeClientRect('zp-page-scroll-top', false).then(node => {
                    const nodeHeight = node ? node[0].height : 0;
                    weexDom.scrollToElement(el, {
                        offset: -nodeHeight,
                        animated: animate
                    });
                });
            } else {
                if (!this.isIos && this.nvueListIs === 'scroller') {
                    this._getNodeClientRect('zp-n-refresh-container', false).then(node => {
                        const nodeHeight = node ? node[0].height : 0;
                        weexDom.scrollToElement(el, {
                            offset: -nodeHeight,
                            animated: animate
                        });
                    });
                } else {
                    weexDom.scrollToElement(el, {
                        offset: 0,
                        animated: animate
                    });
                }
            }
            return;
            // #endif
            if (this.usePageScroll) {
                this.$nextTick(() => {
                    uni.pageScrollTo({
                        scrollTop: 0,
                        duration: animate ? 100 : 0,
                    });
                });
                return;
            }
            this._updatePrivateScrollWithAnimation(animate);
            this.scrollTop = this.oldScrollTop;
            this.$nextTick(() => {
                this.scrollTop = 0;
                this.oldScrollTop = this.scrollTop;
            });
        },
        // æ»šåŠ¨åˆ°åº•éƒ¨
        async _scrollToBottom(animate = true) {
            // #ifdef APP-NVUE
            // åœ¨nvue中需要通过weex.scrollToElement滚动到顶部,此时在底部插入了一个view,使得滚动到这个view位置
            const el = this.$refs['zp-n-list-bottom-tag'];
            if (el) {
                weexDom.scrollToElement(el, {
                    offset: 0,
                    animated: animate
                });
            } else {
                u.consoleErr('滚动到底部失败,因为您设置了hideNvueBottomTag为true');
            }
            return;
            // #endif
            if (this.usePageScroll) {
                this.$nextTick(() => {
                    uni.pageScrollTo({
                        scrollTop: Number.MAX_VALUE,
                        duration: animate ? 100 : 0,
                    });
                });
                return;
            }
            try {
                this._updatePrivateScrollWithAnimation(animate);
                const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container');
                const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
                const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
                const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
                if (pagingContainerH > scrollViewH) {
                    this.scrollTop = this.oldScrollTop;
                    this.$nextTick(() => {
                        this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight;
                        this.oldScrollTop = this.scrollTop;
                    });
                }
            } catch (e) {}
        },
        // æ»šåŠ¨åˆ°æŒ‡å®šview
        _scrollIntoView(sel, offset = 0, animate = false, finishCallback) {
            try {
                this.scrollTop = this.oldScrollTop;
                this.$nextTick(() => {
                    // #ifdef APP-NVUE
                    const refs = this.$parent.$refs;
                    if (!refs) return;
                    const dataType = Object.prototype.toString.call(sel);
                    let el = null;
                    if (dataType === '[object Number]') {
                        const els = refs[`z-paging-${sel}`];
                        el = els ? els[0] : null;
                    } else if (dataType === '[object Array]') {
                        el = sel[0];
                    } else {
                        el = sel;
                    }
                    if (el) {
                        weexDom.scrollToElement(el, {
                            offset: -offset,
                            animated: animate
                        });
                    } else {
                        u.consoleErr('在nvue中滚动到指定位置,cell必须设置 :ref="`z-paging-${index}`"');
                    }
                    return;
                    // #endif
                    this._getNodeClientRect('#' + sel.replace('#', ''), this.$parent).then((node) => {
                        if (node) {
                            let nodeTop = node[0].top;
                            this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
                            finishCallback && finishCallback();
                        }
                    });
                });
            } catch (e) {}
        },
        // é€šè¿‡nodeTop滚动到指定view
        _scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
            // å¦‚果是聊天记录模式并且列表倒置了,此时nodeTop需要等于scroll-view高度 - nodeTop
            if (this.isChatRecordModeAndInversion) {
                this._getNodeClientRect('.zp-scroll-view').then(sNode => {
                    if (sNode) {
                        this._scrollToY(sNode[0].height - nodeTop, offset, animate, true);
                    }
                })
            } else {
                this._scrollToY(nodeTop, offset, animate, true);
            }
        },
        // y轴滚动到指定位置
        _scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
            this._updatePrivateScrollWithAnimation(animate);
            u.delay(() => {
                if (this.usePageScroll) {
                    if (addScrollTop && this.pageScrollTop !== -1) {
                       y += this.pageScrollTop;
                    }
                    const scrollTop = y - offset;
                    uni.pageScrollTo({
                        scrollTop,
                        duration: animate ? 100 : 0
                    });
                } else {
                    if (addScrollTop) {
                       y += this.oldScrollTop;
                    }
                    this.scrollTop = y - offset;
                }
            }, 10)
        },
        // x轴滚动到指定位置
        _scrollToX(x, offset = 0, animate = false) {
            this._updatePrivateScrollWithAnimation(animate);
            u.delay(() => {
                if (!this.usePageScroll) {
                    this.scrollLeft = x - offset;
                } else {
                    u.consoleErr('使用页面滚动时不支持scrollToX');
                }
            }, 10)
        },
        // scroll-view滚动中
        _scroll(e) {
            this.$emit('scroll', e);
            const { scrollTop, scrollLeft } = e.detail;
            // #ifndef APP-NVUE
            this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
            // #endif
            this.oldScrollTop = scrollTop;
            this.oldScrollLeft = scrollLeft;
            // æ»šåŠ¨åŒºåŸŸå†…å®¹çš„æ€»é«˜åº¦ - å½“前滚动的scrollTop = å½“前滚动区域的顶部与内容底部的距离
            const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
            // åœ¨éžios平台滚动中,再次验证一下是否滚动到了底部。因为在一些安卓设备中,有概率滚动到底部不触发@scrolltolower事件,因此添加双重检测逻辑
            !this.isIos && this._checkScrolledToBottom(scrollDiff);
        },
        // emit scrolltolower/scrolltoupper事件
        _emitScrollEvent(type) {
            const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower';
            const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion
                ? reversedType
                : type;
            this.$emit(eventType);
        },
        // æ›´æ–°å†…置的scroll-view是否启用滚动动画
        _updatePrivateScrollWithAnimation(animate) {
            this.privateScrollWithAnimation = animate ? 1 : 0;
            u.delay(() => this.$nextTick(() => {
                // åœ¨æ»šåŠ¨ç»“æŸåŽå°†æ»šåŠ¨åŠ¨ç”»çŠ¶æ€è®¾ç½®å›žåˆå§‹çŠ¶æ€
                this.privateScrollWithAnimation = -1;
            }), 100, 'updateScrollWithAnimationDelay')
        },
        // æ£€æµ‹scrollView是否要铺满屏幕
        _doCheckScrollViewShouldFullHeight(totalData) {
            if (this.autoFullHeight && this.usePageScroll && this.isTotalChangeFromAddData) {
                // #ifndef APP-NVUE
                this.$nextTick(() => {
                    this._checkScrollViewShouldFullHeight((scrollViewNode, pagingContainerNode) => {
                        this._preCheckShowNoMoreInside(totalData, scrollViewNode, pagingContainerNode)
                    });
                })
                // #endif
                // #ifdef APP-NVUE
                this._preCheckShowNoMoreInside(totalData)
                // #endif
            } else {
                this._preCheckShowNoMoreInside(totalData)
            }
        },
        // æ£€æµ‹z-paging是否要全屏覆盖(当使用页面滚动并且不满全屏时,默认z-paging需要铺满全屏,避免数据过少时内部的empty-view无法正确展示)
        async _checkScrollViewShouldFullHeight(callback) {
            try {
                const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
                const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container-content');
                if (!scrollViewNode || !pagingContainerNode) return;
                const scrollViewHeight = pagingContainerNode[0].height;
                const scrollViewTop = scrollViewNode[0].top;
                if (this.isAddedData && scrollViewHeight + scrollViewTop <= this.windowHeight) {
                    this._setAutoHeight(true, scrollViewNode);
                    callback(scrollViewNode, pagingContainerNode);
                } else {
                    this._setAutoHeight(false);
                    callback(null, null);
                }
            } catch (e) {
                callback(null, null);
            }
        },
        // æ›´æ–°ç¼“存中z-paging整个内容容器高度
        async _updateCachedSuperContentHeight() {
            const superContentNode = await this._getNodeClientRect('.z-paging-content');
            if (superContentNode) {
                this.superContentHeight = superContentNode[0].height;
            }
        },
        // scrollTop改变时触发
        _scrollTopChange(newVal, isPageScrollTop){
            this.$emit('scrollTopChange', newVal);
            this.$emit('update:scrollTop', newVal);
            this._checkShouldShowBackToTop(newVal);
            // ä¹‹å‰åœ¨å®‰å“中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,因此判断scrollTop在105之内都允许下拉刷新,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
            // const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : (newVal > 105 ? 106 : (newVal > 5 ? 6 : 0));
            const scrollTop = newVal > 5 ? 6 : 0;
            if (isPageScrollTop && this.wxsPageScrollTop !== scrollTop) {
                this.wxsPageScrollTop = scrollTop;
            } else if (!isPageScrollTop && this.wxsScrollTop !== scrollTop) {
                this.wxsScrollTop = scrollTop;
                if (scrollTop > 6) {
                    this.scrollEnable = true;
                }
            }
        },
        // æ›´æ–°ä½¿ç”¨é¡µé¢æ»šåŠ¨æ—¶slot="top"或"bottom"插入view的高度
        _updatePageScrollTopOrBottomHeight(type) {
            // #ifndef APP-NVUE
            if (!this.usePageScroll) return;
            // #endif
            this._doCheckScrollViewShouldFullHeight(this.realTotalData);
            const node = `.zp-page-${type}`;
            const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
            let safeAreaInsetBottomAdd = this.safeAreaInsetBottom;
            this.$nextTick(() => {
                let delayTime = 0;
                // #ifdef MP-BAIDU || APP-NVUE
                delayTime = 50;
                // #endif
                u.delay(() => {
                    this._getNodeClientRect(node).then((res) => {
                        if (res) {
                            let pageScrollNodeHeight = res[0].height;
                            if (type === 'bottom') {
                                if (safeAreaInsetBottomAdd) {
                                    pageScrollNodeHeight += this.safeAreaBottom;
                                }
                            } else {
                                this.cacheTopHeight = pageScrollNodeHeight;
                            }
                            this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`);
                        } else if (safeAreaInsetBottomAdd) {
                            this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`);
                        }
                    });
                }, delayTime)
            })
        },
    }
}
src/components/z-paging/js/modules/virtual-list.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,555 @@
// [z-paging]虚拟列表模块
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import Enum from '.././z-paging-enum'
export default {
    props: {
        // æ˜¯å¦ä½¿ç”¨è™šæ‹Ÿåˆ—表,默认为否
        useVirtualList: {
            type: Boolean,
            default: u.gc('useVirtualList', false)
        },
        // åœ¨ä½¿ç”¨è™šæ‹Ÿåˆ—表时,是否使用兼容模式,默认为否
        useCompatibilityMode: {
            type: Boolean,
            default: u.gc('useCompatibilityMode', false)
        },
        // ä½¿ç”¨å…¼å®¹æ¨¡å¼æ—¶ä¼ é€’的附加数据
        extraData: {
            type: Object,
            default: u.gc('extraData', {})
        },
        // æ˜¯å¦åœ¨z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
        useInnerList: {
            type: Boolean,
            default: u.gc('useInnerList', false)
        },
        // å¼ºåˆ¶å…³é—­inner-list,默认为false,如果为true将强制关闭innerList,适用于开启了虚拟列表后需要强制关闭inner-list的情况
        forceCloseInnerList: {
            type: Boolean,
            default: u.gc('forceCloseInnerList', false)
        },
        // å†…置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
        cellKeyName: {
            type: String,
            default: u.gc('cellKeyName', '')
        },
        // innerList样式
        innerListStyle: {
            type: Object,
            default: u.gc('innerListStyle', {})
        },
        // innerCell样式
        innerCellStyle: {
            type: Object,
            default: u.gc('innerCellStyle', {})
        },
        // é¢„加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
        preloadPage: {
            type: [Number, String],
            default: u.gc('preloadPage', 12),
            validator: (value) => {
                if (value <= 0) u.consoleErr('preload-page必须大于0!');
                return value > 0;
            }
        },
        // è™šæ‹Ÿåˆ—表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
        cellHeightMode: {
            type: String,
            default: u.gc('cellHeightMode', Enum.CellHeightMode.Fixed)
        },
        // å›ºå®šçš„cell高度,cellHeightMode=fixed才有效,若设置了值,则不计算第一个cell高度而使用设置的cell高度
        fixedCellHeight: {
            type: [Number, String],
            default: u.gc('fixedCellHeight', 0)
        },
        // è™šæ‹Ÿåˆ—表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
        virtualListCol: {
            type: [Number, String],
            default: u.gc('virtualListCol', 1)
        },
        // è™šæ‹Ÿåˆ—表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
        virtualScrollFps: {
            type: [Number, String],
            default: u.gc('virtualScrollFps', 80)
        },
        // è™šæ‹Ÿåˆ—表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id,注意:请勿传数字或以数字开头的字符串。如设置为list1,则cell的id应为:list1-zp-id-${item.zp_index}
        virtualCellIdPrefix: {
            type: String,
            default: u.gc('virtualCellIdPrefix', '')
        },
        // è™šæ‹Ÿåˆ—表是否使用swiper-item包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
        // ä»…vue3+(微信小程序或QQ小程序)+非内置列表写法虚拟列表有效,其他情况此属性设置任何值都无效,所以如果您在swiper-item内使用z-paging的非内置虚拟列表写法,将此属性设置为true即可
        virtualInSwiperSlot: {
            type: Boolean,
            default: false
        },
    },
    data() {
        return {
            virtualListKey: u.getInstanceId(),
            virtualPageHeight: 0,
            virtualCellHeight: 0,
            virtualScrollTimeStamp: 0,
            virtualList: [],
            virtualPlaceholderTopHeight: 0,
            virtualPlaceholderBottomHeight: 0,
            virtualTopRangeIndex: 0,
            virtualBottomRangeIndex: 0,
            lastVirtualTopRangeIndex: 0,
            lastVirtualBottomRangeIndex: 0,
            virtualItemInsertedCount: 0,
            virtualHeightCacheList: [],
            getCellHeightRetryCount: {
                fixed: 0,
                dynamic: 0
            },
            pagingOrgTop: -1,
            updateVirtualListFromDataChange: false
        }
    },
    watch: {
        // ç›‘听总数据的改变,刷新虚拟列表布局
        realTotalData() {
            this.updateVirtualListRender();
        },
        // ç›‘听虚拟列表渲染数组的改变并emit
        virtualList(newVal){
            this.$emit('update:virtualList', newVal);
            this.$emit('virtualListChange', newVal);
        },
        // ç›‘听虚拟列表顶部占位高度改变并emit
        virtualPlaceholderTopHeight(newVal) {
            this.$emit('virtualTopHeightChange', newVal);
        }
    },
    computed: {
        virtualCellIndexKey() {
            return c.listCellIndexKey;
        },
        finalUseVirtualList() {
            if (this.useVirtualList && this.usePageScroll){
                u.consoleErr('使用页面滚动时,开启虚拟列表无效!');
            }
            return this.useVirtualList && !this.usePageScroll;
        },
        finalUseInnerList() {
            return this.useInnerList || (this.finalUseVirtualList && !this.forceCloseInnerList);
        },
        finalCellKeyName() {
            // #ifdef APP-NVUE
            if (this.finalUseVirtualList && !this.cellKeyName.length){
                u.consoleErr('在nvue中开启use-virtual-list必须设置cell-key-name,否则将可能导致列表渲染错误!');
            }
            // #endif
            return this.cellKeyName;
        },
        finalVirtualPageHeight(){
            return this.virtualPageHeight > 0 ? this.virtualPageHeight : this.windowHeight;
        },
        finalFixedCellHeight() {
            return u.convertToPx(this.fixedCellHeight);
        },
        fianlVirtualCellIdPrefix() {
            const prefix = this.virtualCellIdPrefix ? this.virtualCellIdPrefix + '-' : '';
            return prefix + 'zp-id';
        },
        finalPlaceholderTopHeightStyle() {
            // #ifdef VUE2
            return { transform: this.virtualPlaceholderTopHeight > 0 ? `translateY(${this.virtualPlaceholderTopHeight}px)` : 'none' };
            // #endif
            return {};
        },
        virtualRangePageHeight(){
            return this.finalVirtualPageHeight * this.preloadPage;
        },
        virtualScrollDisTimeStamp() {
            return 1000 / this.virtualScrollFps;
        }
    },
    methods: {
        // åœ¨ä½¿ç”¨åŠ¨æ€é«˜åº¦è™šæ‹Ÿåˆ—è¡¨æ—¶ï¼Œè‹¥åœ¨åˆ—è¡¨æ•°ç»„ä¸­éœ€è¦æ’å…¥æŸä¸ªitem,需要调用此方法;item:需要插入的item,index:插入的cell位置,若index为2,则插入的item在原list的index=1之后,index从0开始
        doInsertVirtualListItem(item, index) {
            if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
            this.realTotalData.splice(index, 0, item);
            // #ifdef VUE3
            this.realTotalData = [...this.realTotalData];
            // #endif
            this.virtualItemInsertedCount ++;
            if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
                item = { item };
            }
            const cellIndexKey = this.virtualCellIndexKey;
            item[cellIndexKey] = `custom-${this.virtualItemInsertedCount}`;
            item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
            this.$nextTick(async () => {
                let retryCount = 0;
                while (retryCount <= 10) {
                    await u.wait(c.delayTime);
                    const cellNode = await this._getVirtualCellNodeByIndex(item[cellIndexKey]);
                    // å¦‚果获取当前cell的节点信息失败,则重试(不超过10次)
                    if (!cellNode) {
                        retryCount ++;
                        continue;
                    }
                    const currentHeight = cellNode ? cellNode[0].height : 0;
                    const lastHeightCache = this.virtualHeightCacheList[index - 1];
                    const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
                    // åœ¨ç¼“存的cell高度数组中,插入此cell高度信息
                    this.virtualHeightCacheList.splice(index, 0, {
                        height: currentHeight,
                        lastTotalHeight,
                        totalHeight: lastTotalHeight + currentHeight
                    });
                    // ä»Žå½“前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell的高度
                    for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
                        const thisNode = this.virtualHeightCacheList[i];
                        thisNode.lastTotalHeight += currentHeight;
                        thisNode.totalHeight += currentHeight;
                    }
                    this._updateVirtualScroll(this.oldScrollTop);
                    break;
                }
            })
        },
        // åœ¨ä½¿ç”¨åŠ¨æ€é«˜åº¦è™šæ‹Ÿåˆ—è¡¨æ—¶ï¼Œæ‰‹åŠ¨æ›´æ–°æŒ‡å®šcell的缓存高度(当cell高度在初始化之后再次改变后调用);index:需要更新的cell在列表中的位置,从0开始
        didUpdateVirtualListCell(index) {
            if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
            const currentNode = this.virtualHeightCacheList[index];
            this.$nextTick(() => {
                this._getVirtualCellNodeByIndex(index).then(cellNode => {
                    // æ›´æ–°å½“前cell的高度
                    const cellNodeHeight = cellNode ? cellNode[0].height : 0;
                    const heightDis = cellNodeHeight - currentNode.height;
                    currentNode.height = cellNodeHeight;
                    currentNode.totalHeight = currentNode.lastTotalHeight + cellNodeHeight;
                    // ä»Žå½“前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell变化的高度
                    for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
                        const thisNode = this.virtualHeightCacheList[i];
                        thisNode.totalHeight += heightDis;
                        thisNode.lastTotalHeight += heightDis;
                    }
                });
            })
        },
        // åœ¨ä½¿ç”¨åŠ¨æ€é«˜åº¦è™šæ‹Ÿåˆ—è¡¨æ—¶ï¼Œè‹¥åˆ é™¤äº†åˆ—è¡¨æ•°ç»„ä¸­çš„æŸä¸ªitem,需要调用此方法以更新高度缓存数组;index:删除的cell在列表中的位置,从0开始
        didDeleteVirtualListCell(index) {
            if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
            const currentNode = this.virtualHeightCacheList[index];
            // ä»Žå½“前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要减去当前cell的高度
            for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
                const thisNode = this.virtualHeightCacheList[i];
                thisNode.totalHeight -= currentNode.height;
                thisNode.lastTotalHeight -= currentNode.height;
            }
            // å°†å½“前cell的高度信息从高度缓存数组中删除
            this.virtualHeightCacheList.splice(index, 1);
        },
        // æ‰‹åŠ¨è§¦å‘è™šæ‹Ÿåˆ—è¡¨æ¸²æŸ“æ›´æ–°ï¼Œå¯ç”¨äºŽè§£å†³ä¾‹å¦‚ä¿®æ”¹äº†è™šæ‹Ÿåˆ—è¡¨æ•°ç»„ä¸­å…ƒç´ ï¼Œä½†å±•ç¤ºæœªæ›´æ–°çš„æƒ…å†µ
        updateVirtualListRender() {
            // #ifndef APP-NVUE
            if (this.finalUseVirtualList) {
                this.updateVirtualListFromDataChange = true;
                this.$nextTick(() => {
                    this.getCellHeightRetryCount.fixed = 0;
                    if (this.realTotalData.length) {
                        this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight()
                    } else {
                        this._resetDynamicListState(!this.isUserPullDown);
                    }
                    this._updateVirtualScroll(this.oldScrollTop);
                })
            }
            // #endif
        },
        // åˆå§‹åŒ–虚拟列表
        _virtualListInit() {
            this.$nextTick(() => {
                u.delay(() => {
                    // èŽ·å–è™šæ‹Ÿåˆ—è¡¨æ»šåŠ¨åŒºåŸŸçš„é«˜åº¦
                    this._getNodeClientRect('.zp-scroll-view').then(node => {
                        if (node) {
                            this.pagingOrgTop = node[0].top;
                            this.virtualPageHeight = node[0].height;
                        }
                    });
                });
            })
        },
        // cellHeightMode为fixed时获取第一个cell高度
        _updateFixedCellHeight() {
            if (!this.finalFixedCellHeight) {
                this.$nextTick(() => {
                    u.delay(() => {
                        this._getVirtualCellNodeByIndex(0).then(cellNode => {
                            if (!cellNode) {
                                if (this.getCellHeightRetryCount.fixed > 10) return;
                                this.getCellHeightRetryCount.fixed ++;
                                // å¦‚果获取第一个cell的节点信息失败,则重试(不超过10次)
                                this._updateFixedCellHeight();
                            } else {
                                this.virtualCellHeight = cellNode[0].height;
                                this._updateVirtualScroll(this.oldScrollTop);
                            }
                        });
                    }, c.delayTime, 'updateFixedCellHeightDelay');
                })
            } else {
                this.virtualCellHeight = this.finalFixedCellHeight;
            }
        },
        // cellHeightMode为dynamic时获取每个cell高度
        _updateDynamicCellHeight(list, dataFrom = 'bottom') {
            const dataFromTop = dataFrom === 'top';
            const heightCacheList = this.virtualHeightCacheList;
            const currentCacheList = dataFromTop ?  [] : heightCacheList;
            let listTotalHeight = 0;
            this.$nextTick(() => {
                u.delay(async () => {
                    for (let i = 0; i < list.length; i++) {
                        const cellNode = await this._getVirtualCellNodeByIndex(list[i][this.virtualCellIndexKey]);
                        const currentHeight = cellNode ? cellNode[0].height : 0;
                        if (!cellNode) {
                            if (this.getCellHeightRetryCount.dynamic <= 10) {
                                heightCacheList.splice(heightCacheList.length - i, i);
                                this.getCellHeightRetryCount.dynamic ++;
                                // å¦‚果获取当前cell的节点信息失败,则重试(不超过10次)
                                this._updateDynamicCellHeight(list, dataFrom);
                            }
                            return;
                        }
                        const lastHeightCache = currentCacheList.length ? currentCacheList.slice(-1)[0] : null;
                        const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
                        // ç¼“存当前cell的高度信息:height-当前cell高度;lastTotalHeight-前面所有cell的高度总和;totalHeight-包含当前cell的所有高度总和
                        currentCacheList.push({
                            height: currentHeight,
                            lastTotalHeight,
                            totalHeight: lastTotalHeight + currentHeight
                        });
                        if (dataFromTop) {
                            listTotalHeight += currentHeight;
                        }
                    }
                    // å¦‚果数据是从顶部拼接的
                    if (dataFromTop && list.length) {
                        for (let i = 0; i < heightCacheList.length; i++) {
                            // æ›´æ–°ä¹‹å‰æ‰€æœ‰é¡¹çš„缓存高度,需要加上此次插入的所有cell高度之和(因为是从顶部插入的cell)
                            const heightCacheItem = heightCacheList[i];
                            heightCacheItem.lastTotalHeight += listTotalHeight;
                            heightCacheItem.totalHeight += listTotalHeight;
                        }
                        this.virtualHeightCacheList = currentCacheList.concat(heightCacheList);
                    }
                    this._updateVirtualScroll(this.oldScrollTop);
                }, c.delayTime, 'updateDynamicCellHeightDelay')
            })
        },
        // è®¾ç½®cellItem的index
        _setCellIndex(list, dataFrom = 'bottom') {
            let currentItemIndex = 0;
            const cellIndexKey = this.virtualCellIndexKey;
            dataFrom === 'bottom' && ([Enum.QueryFrom.Refresh, Enum.QueryFrom.Reload].indexOf(this.queryFrom) >= 0) && this._resetDynamicListState();
            if (this.totalData.length && this.queryFrom !== Enum.QueryFrom.Refresh) {
                if (dataFrom === 'bottom') {
                    currentItemIndex = this.realTotalData.length;
                    const lastItem = this.realTotalData.length ? this.realTotalData.slice(-1)[0] : null;
                    if (lastItem && lastItem[cellIndexKey] !== undefined) {
                        currentItemIndex = lastItem[cellIndexKey] + 1;
                    }
                } else if (dataFrom === 'top') {
                    const firstItem = this.realTotalData.length ? this.realTotalData[0] : null;
                    if (firstItem && firstItem[cellIndexKey] !== undefined) {
                        currentItemIndex = firstItem[cellIndexKey] - list.length;
                    }
                }
            } else {
                this._resetDynamicListState();
            }
            for (let i = 0; i < list.length; i++) {
                let item = list[i];
                if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
                    item = { item };
                }
                if (item[c.listCellIndexUniqueKey]) {
                    item = u.deepCopy(item);
                }
                item[cellIndexKey] = currentItemIndex + i;
                item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
                list[i] = item;
            }
            this.getCellHeightRetryCount.dynamic = 0;
            this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list, dataFrom);
        },
        // æ›´æ–°scroll滚动(虚拟列表滚动时触发)
        _updateVirtualScroll(scrollTop, scrollDiff = 0) {
            const currentTimeStamp = u.getTime();
            scrollTop === 0 && this._resetTopRange();
            if (scrollTop !== 0 && this.virtualScrollTimeStamp && currentTimeStamp - this.virtualScrollTimeStamp <= this.virtualScrollDisTimeStamp) {
                return;
            }
            this.virtualScrollTimeStamp = currentTimeStamp;
            let scrollIndex = 0;
            const cellHeightMode = this.cellHeightMode;
            if (cellHeightMode === Enum.CellHeightMode.Fixed) {
                // å¦‚果是固定高度的虚拟列表
                // è®¡ç®—当前滚动到的cell的index = scrollTop / è™šæ‹Ÿåˆ—表cell的固定高度
                scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
                // æ›´æ–°é¡¶éƒ¨å’Œåº•部占位view的高度(为兼容考虑,顶部采用transformY的方式占位)
                this._updateFixedTopRangeIndex(scrollIndex);
                this._updateFixedBottomRangeIndex(scrollIndex);
            } else if(cellHeightMode === Enum.CellHeightMode.Dynamic) {
                // å¦‚果是不固定高度的虚拟列表
                // å½“前滚动的方向
                const scrollDirection = scrollDiff > 0 ? 'top' : 'bottom';
                // è§†å›¾åŒºåŸŸçš„高度
                const rangePageHeight = this.virtualRangePageHeight;
                // é¡¶éƒ¨è§†å›¾åŒºåŸŸå¤–的高度(顶部不需要渲染而是需要占位部分的高度)
                const topRangePageOffset = scrollTop - rangePageHeight;
                // åº•部视图区域外的高度(底部不需要渲染而是需要占位部分的高度)
                const bottomRangePageOffset = scrollTop + this.finalVirtualPageHeight + rangePageHeight;
                let virtualBottomRangeIndex = 0;
                let virtualPlaceholderBottomHeight = 0;
                let reachedLimitBottom = false;
                const heightCacheList = this.virtualHeightCacheList;
                const lastHeightCache = !!heightCacheList ? heightCacheList.slice(-1)[0] : null;
                let startTopRangeIndex = this.virtualTopRangeIndex;
                // å¦‚果是向底部滚动(顶部占位的高度不断增大,顶部的实际渲染cell数量不断减少)
                if (scrollDirection === 'bottom') {
                    // ä»Žé¡¶éƒ¨è§†å›¾è¾¹ç¼˜çš„cell的位置开始向后查找
                    for (let i = startTopRangeIndex; i < heightCacheList.length; i++){
                        const heightCacheItem = heightCacheList[i];
                        // å¦‚果查找到某个cell对应的totalHeight大于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
                        if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
                            // è®°å½•顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
                            this.virtualTopRangeIndex = i;
                            this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
                            break;
                        }
                    }
                } else {
                    // å¦‚果是向顶部滚动(顶部占位的高度不断减少,顶部的实际渲染cell数量不断增加)
                    let topRangeMatched = false;
                    // ä»Žé¡¶éƒ¨è§†å›¾è¾¹ç¼˜çš„cell的位置开始向前查找
                    for (let i = startTopRangeIndex; i >= 0; i--){
                        const heightCacheItem = heightCacheList[i];
                        // å¦‚果查找到某个cell对应的totalHeight小于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
                        if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
                            // è®°å½•顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
                            this.virtualTopRangeIndex = i;
                            this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
                            topRangeMatched = true;
                            break;
                        }
                    }
                    // å¦‚果查找不到,则认为顶部占位高度为0了,顶部cell不需要继续复用,重置topRangeIndex和placeholderTopHeight
                    !topRangeMatched && this._resetTopRange();
                }
                // ä»Žé¡¶éƒ¨è§†å›¾è¾¹ç¼˜çš„cell的位置开始向后查找
                for (let i = this.virtualTopRangeIndex; i < heightCacheList.length; i++){
                    const heightCacheItem = heightCacheList[i];
                    // å¦‚果查找到某个cell对应的totalHeight大于底部视图区域外的高度,则此cell为底部视图边缘的cell
                    if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
                        // è®°å½•底部视图边缘cell的index并更新底部占位区域的高度并停止继续查找
                        virtualBottomRangeIndex = i;
                        virtualPlaceholderBottomHeight = lastHeightCache.totalHeight - heightCacheItem.totalHeight;
                        reachedLimitBottom = true;
                        break;
                    }
                }
                if (!reachedLimitBottom || this.virtualBottomRangeIndex === 0) {
                    this.virtualBottomRangeIndex = this.realTotalData.length ? this.realTotalData.length - 1 : this.pageSize;
                    this.virtualPlaceholderBottomHeight = 0;
                } else {
                    this.virtualBottomRangeIndex = virtualBottomRangeIndex;
                    this.virtualPlaceholderBottomHeight = virtualPlaceholderBottomHeight;
                }
                this._updateVirtualList();
            }
        },
        // æ›´æ–°fixedCell模式下topRangeIndex&placeholderTopHeight
        _updateFixedTopRangeIndex(scrollIndex) {
            let virtualTopRangeIndex = this.virtualCellHeight === 0 ? 0 : scrollIndex - (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * this.preloadPage;
            virtualTopRangeIndex *= this.virtualListCol;
            virtualTopRangeIndex = Math.max(0, virtualTopRangeIndex);
            this.virtualTopRangeIndex = virtualTopRangeIndex;
            this.virtualPlaceholderTopHeight = (virtualTopRangeIndex / this.virtualListCol) * this.virtualCellHeight;
        },
        // æ›´æ–°fixedCell模式下bottomRangeIndex&placeholderBottomHeight
        _updateFixedBottomRangeIndex(scrollIndex) {
            let virtualBottomRangeIndex = this.virtualCellHeight === 0 ? this.pageSize : scrollIndex + (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * (this.preloadPage + 1);
            virtualBottomRangeIndex *= this.virtualListCol;
            virtualBottomRangeIndex = Math.min(this.realTotalData.length, virtualBottomRangeIndex);
            this.virtualBottomRangeIndex = virtualBottomRangeIndex;
            this.virtualPlaceholderBottomHeight = (this.realTotalData.length - virtualBottomRangeIndex) * this.virtualCellHeight / this.virtualListCol;
            this._updateVirtualList();
        },
        // æ›´æ–°virtualList
        _updateVirtualList() {
            const shouldUpdateList = this.updateVirtualListFromDataChange || (this.lastVirtualTopRangeIndex !== this.virtualTopRangeIndex || this.lastVirtualBottomRangeIndex !== this.virtualBottomRangeIndex);
            if (shouldUpdateList) {
                this.updateVirtualListFromDataChange = false;
                this.lastVirtualTopRangeIndex =  this.virtualTopRangeIndex;
                this.lastVirtualBottomRangeIndex = this.virtualBottomRangeIndex;
                this.virtualList = this.realTotalData.slice(this.virtualTopRangeIndex, this.virtualBottomRangeIndex + 1);
            }
        },
        // é‡ç½®åŠ¨æ€cell模式下的高度缓存数据、虚拟列表和滚动状态
        _resetDynamicListState(resetVirtualList = false) {
            this.virtualHeightCacheList = [];
            if (resetVirtualList) {
                this.virtualList = [];
            }
            this.virtualTopRangeIndex = 0;
            this.virtualPlaceholderTopHeight = 0;
        },
        // é‡ç½®topRangeIndex和placeholderTopHeight
        _resetTopRange() {
            this.virtualTopRangeIndex = 0;
            this.virtualPlaceholderTopHeight = 0;
            this._updateVirtualList();
        },
        // æ£€æµ‹è™šæ‹Ÿåˆ—表当前滚动位置,如发现滚动位置不正确则重新计算虚拟列表相关参数(为解决在App中可能出现的长时间进入后台后打开App白屏的问题)
        _checkVirtualListScroll() {
            if (this.finalUseVirtualList) {
                this.$nextTick(() => {
                    this._getNodeClientRect('.zp-paging-touch-view').then(node => {
                        const currentTop = node ? node[0].top : 0;
                        if (!node || (currentTop === this.pagingOrgTop && this.virtualPlaceholderTopHeight !== 0)) {
                            this._updateVirtualScroll(0);
                        }
                    });
                })
            }
        },
        // èŽ·å–å¯¹åº”index的虚拟列表cell节点信息
        _getVirtualCellNodeByIndex(index) {
            let inDom = this.finalUseInnerList;
            // åœ¨vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度的问题
            // é€šè¿‡uni.createSelectorQuery().in(this.$parent)来解决此问题
            // #ifdef VUE3
            // #ifdef MP-WEIXIN || MP-QQ
            if (this.forceCloseInnerList && this.virtualInSwiperSlot) {
                inDom = this.$parent;
            }
            // #endif
            // #endif
            return this._getNodeClientRect(`#${this.fianlVirtualCellIdPrefix}-${index}`, inDom);
        },
        // å¤„理使用内置列表时点击了cell事件
        _innerCellClick(item, index) {
            this.$emit('innerCellClick', item, index);
        }
    }
}
src/components/z-paging/js/z-paging-constant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
// [z-paging]常量
export default {
    // å½“前版本号
    version: '2.8.6',
    // å»¶è¿Ÿæ“ä½œçš„通用时间
    delayTime: 100,
    // è¯·æ±‚失败时候全局emit使用的key
    errorUpdateKey: 'z-paging-error-emit',
    // å…¨å±€emit complete的key
    completeUpdateKey: 'z-paging-complete-emit',
    // z-paging缓存的前缀key
    cachePrefixKey: 'z-paging-cache',
    // è™šæ‹Ÿåˆ—表中列表index的key
    listCellIndexKey: 'zp_index',
    // è™šæ‹Ÿåˆ—表中列表的唯一key
    listCellIndexUniqueKey: 'zp_unique_index'
}
src/components/z-paging/js/z-paging-enum.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
// [z-paging]枚举
export default {
    // å½“前加载类型 refresher:下拉刷新 load-more:上拉加载更多
    LoadingType: {
        Refresher: 'refresher',
        LoadMore: 'load-more'
    },
    // ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€ default:默认状态 release-to-refresh:松手立即刷新 loading:刷新中 complete:刷新结束 go-f2:松手进入二楼
    Refresher: {
        Default: 'default',
        ReleaseToRefresh: 'release-to-refresh',
        Loading: 'loading',
        Complete: 'complete',
        GoF2: 'go-f2'
    },
    // åº•部加载更多状态 default:默认状态 loading:加载中 no-more:没有更多数据 fail:加载失败
    More: {
        Default: 'default',
        Loading: 'loading',
        NoMore: 'no-more',
        Fail: 'fail'
    },
    // @query触发来源 user-pull-down:用户主动下拉刷新 reload:通过reload触发 refresh:通过refresh触发 load-more:通过滚动到底部加载更多或点击底部加载更多触发
    QueryFrom: {
        UserPullDown: 'user-pull-down',
        Reload: 'reload',
        Refresh: 'refresh',
        LoadMore: 'load-more'
    },
    // è™šæ‹Ÿåˆ—表cell高度模式
    CellHeightMode: {
        // å›ºå®šé«˜åº¦
        Fixed: 'fixed',
        // åŠ¨æ€é«˜åº¦
        Dynamic: 'dynamic'
    },
    // åˆ—表缓存模式
    CacheMode: {
        // é»˜è®¤æ¨¡å¼ï¼Œåªä¼šç¼“存一次
        Default: 'default',
        // æ€»æ˜¯ç¼“存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
        Always: 'always'
    }
}
src/components/z-paging/js/z-paging-interceptor.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
// [z-paging]拦截器
const queryKey = 'Query';
const fetchParamsKey = 'FetchParams';
const fetchResultKey = 'FetchResult';
const language2LocalKey = 'Language2Local';
// æ‹¦æˆª&处理@query事件
function handleQuery(callback) {
    _addHandleByKey(queryKey, callback);
    return this;
}
// æ‹¦æˆª&处理@query事件(私有,请勿调用)
function _handleQuery(pageNo, pageSize, from, lastItem) {
    const callback = _getHandleByKey(queryKey);
    return callback ? callback(pageNo, pageSize, from, lastItem) : [pageNo, pageSize, from];
}
// æ‹¦æˆª&处理:fetch参数
function handleFetchParams(callback) {
    _addHandleByKey(fetchParamsKey, callback);
    return this;
}
// æ‹¦æˆª&处理:fetch参数(私有,请勿调用)
function _handleFetchParams(parmas, extraParams) {
    const callback = _getHandleByKey(fetchParamsKey);
    return callback ? callback(parmas, extraParams || {}) : { pageNo: parmas.pageNo, pageSize: parmas.pageSize, ...(extraParams || {}) };
}
// æ‹¦æˆª&处理:fetch结果
function handleFetchResult(callback) {
    _addHandleByKey(fetchResultKey, callback);
    return this;
}
// æ‹¦æˆª&处理:fetch结果(私有,请勿调用)
function _handleFetchResult(result, paging, params) {
    const callback = _getHandleByKey(fetchResultKey);
    callback && callback(result, paging, params);
    return callback ? true : false;
}
// æ‹¦æˆª&处理系统language转i18n local
function handleLanguage2Local(callback) {
    _addHandleByKey(language2LocalKey, callback);
    return this;
}
// æ‹¦æˆª&处理系统language转i18n local(私有,请勿调用)
function _handleLanguage2Local(language, local) {
    const callback = _getHandleByKey(language2LocalKey);
    return callback ? callback(language, local) : local;
}
// èŽ·å–å½“å‰app对象
function _getApp(){
    // #ifndef APP-NVUE
    return getApp();
    // #endif
    // #ifdef APP-NVUE
    return getApp({ allowDefault: true });
    // #endif
}
// æ˜¯å¦å¯ä»¥è®¿é—®globalData
function _hasGlobalData() {
    return _getApp() && _getApp().globalData;
}
// æ·»åŠ å¤„ç†å‡½æ•°
function _addHandleByKey(key, callback) {
    try {
        setTimeout(function() {
            if (_hasGlobalData()) {
                _getApp().globalData[`zp_handle${key}Callback`] = callback;
            }
        }, 1);
    } catch (_) {}
}
// èŽ·å–å¤„ç†å›žè°ƒå‡½æ•°
function _getHandleByKey(key) {
    return _hasGlobalData() ? _getApp().globalData[`zp_handle${key}Callback`] : null;
}
export default {
    handleQuery,
    _handleQuery,
    handleFetchParams,
    _handleFetchParams,
    handleFetchResult,
    _handleFetchResult,
    handleLanguage2Local,
    _handleLanguage2Local
};
src/components/z-paging/js/z-paging-main.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,515 @@
// [z-paging]核心js
import zStatic from './z-paging-static'
import c from './z-paging-constant'
import u from './z-paging-utils'
import zPagingRefresh from '../components/z-paging-refresh'
import zPagingLoadMore from '../components/z-paging-load-more'
import zPagingEmptyView from '../../z-paging-empty-view/z-paging-empty-view'
// modules
import commonLayoutModule from './modules/common-layout'
import dataHandleModule from './modules/data-handle'
import i18nModule from './modules/i18n'
import nvueModule from './modules/nvue'
import emptyModule from './modules/empty'
import refresherModule from './modules/refresher'
import loadMoreModule from './modules/load-more'
import loadingModule from './modules/loading'
import chatRecordModerModule from './modules/chat-record-mode'
import scrollerModule from './modules/scroller'
import backToTopModule from './modules/back-to-top'
import virtualListModule from './modules/virtual-list'
import Enum from './z-paging-enum'
const systemInfo = u.getSystemInfoSync();
export default {
    name: "z-paging",
    components: {
        zPagingRefresh,
        zPagingLoadMore,
        zPagingEmptyView
    },
    mixins: [
        commonLayoutModule,
        dataHandleModule,
        i18nModule,
        nvueModule,
        emptyModule,
        refresherModule,
        loadMoreModule,
        loadingModule,
        chatRecordModerModule,
        scrollerModule,
        backToTopModule,
        virtualListModule
    ],
    data() {
        return {
            // --------------静态资源---------------
            base64BackToTop: zStatic.base64BackToTop,
            // -------------全局数据相关--------------
            // å½“前加载类型
            loadingType: Enum.LoadingType.Refresher,
            requestTimeStamp: 0,
            wxsPropType: '',
            renderPropScrollTop: -1,
            checkScrolledToBottomTimeOut: null,
            cacheTopHeight: -1,
            statusBarHeight: systemInfo.statusBarHeight,
            // --------------状态&判断---------------
            insideOfPaging: -1,
            isLoadFailed: false,
            isIos: systemInfo.platform === 'ios',
            disabledBounce: false,
            fromCompleteEmit: false,
            disabledCompleteEmit: false,
            pageLaunched: false,
            active: false,
            // ---------------wxs相关---------------
            wxsIsScrollTopInTopRange: true,
            wxsScrollTop: 0,
            wxsPageScrollTop: 0,
            wxsOnPullingDown: false,
        };
    },
    props: {
        // è°ƒç”¨complete后延迟处理的时间,单位为毫秒,默认0毫秒,优先级高于minDelay
        delay: {
            type: [Number, String],
            default: u.gc('delay', 0),
        },
        // è§¦å‘@query后最小延迟处理的时间,单位为毫秒,默认0毫秒,优先级低于delay(假设设置为300毫秒,若分页请求时间小于300毫秒,则在调用complete后延迟[300毫秒-请求时长];若请求时长大于300毫秒,则不延迟),当show-refresher-when-reload为true或reload(true)时,其最小值为400
        minDelay: {
            type: [Number, String],
            default: u.gc('minDelay', 0),
        },
        // è®¾ç½®z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
        pagingStyle: {
            type: Object,
            default: u.gc('pagingStyle', {}),
        },
        // z-paging的高度,优先级低于pagingStyle中设置的height;传字符串,如100px、100rpx、100%
        height: {
            type: String,
            default: u.gc('height', '')
        },
        // z-paging的宽度,优先级低于pagingStyle中设置的width;传字符串,如100px、100rpx、100%
        width: {
            type: String,
            default: u.gc('width', '')
        },
        // z-paging的最大宽度,优先级低于pagingStyle中设置的max-width;传字符串,如100px、100rpx、100%。默认为空,也就是铺满窗口宽度,若设置了特定值则会自动添加margin: 0 auto
        maxWidth: {
            type: String,
            default: u.gc('maxWidth', '')
        },
        // z-paging的背景色,优先级低于pagingStyle中设置的background。传字符串,如"#ffffff"
        bgColor: {
            type: String,
            default: u.gc('bgColor', '')
        },
        // è®¾ç½®z-paging的容器(插槽的父view)的style
        pagingContentStyle: {
            type: Object,
            default: u.gc('pagingContentStyle', {}),
        },
        // z-paging是否自动高度,若自动高度则会自动铺满屏幕
        autoHeight: {
            type: Boolean,
            default: u.gc('autoHeight', false)
        },
        // z-paging是否自动高度时,附加的高度,注意添加单位px或rpx,若需要减少高度,则传负数
        autoHeightAddition: {
            type: [Number, String],
            default: u.gc('autoHeightAddition', '0px')
        },
        // loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认black
        defaultThemeStyle: {
            type: String,
            default: u.gc('defaultThemeStyle', 'black')
        },
        // z-paging是否使用fixed布局,若使用fixed布局,则z-paging的父view无需固定高度,z-paging高度默认为100%,默认为是(当使用内置scroll-view滚动时有效)
        fixed: {
            type: Boolean,
            default: u.gc('fixed', true)
        },
        // æ˜¯å¦å¼€å¯åº•部安全区域适配
        safeAreaInsetBottom: {
            type: Boolean,
            default: u.gc('safeAreaInsetBottom', false)
        },
        // å¼€å¯åº•部安全区域适配后,是否使用placeholder形式实现,默认为否。为否时滚动区域会自动避开底部安全区域,也就是所有滚动内容都不会挡住底部安全区域,若设置为是,则滚动时滚动内容会挡住底部安全区域,但是当滚动到底部时才会避开底部安全区域
        useSafeAreaPlaceholder: {
            type: Boolean,
            default: u.gc('useSafeAreaPlaceholder', false)
        },
        // z-paging bottom的背景色,默认透明,传字符串,如"#ffffff"
        bottomBgColor: {
            type: String,
            default: u.gc('bottomBgColor', '')
        },
        // slot="top"的view的z-index,默认为99,仅使用页面滚动时有效
        topZIndex: {
            type: Number,
            default: u.gc('topZIndex', 99)
        },
        // z-paging内容容器父view的z-index,默认为1
        superContentZIndex: {
            type: Number,
            default: u.gc('superContentZIndex', 1)
        },
        // z-paging内容容器部分的z-index,默认为1
        contentZIndex: {
            type: Number,
            default: u.gc('contentZIndex', 1)
        },
        // z-paging二楼的z-index,默认为100
        f2ZIndex: {
            type: Number,
            default: u.gc('f2ZIndex', 100)
        },
        // ä½¿ç”¨é¡µé¢æ»šåŠ¨æ—¶ï¼Œæ˜¯å¦åœ¨ä¸æ»¡å±æ—¶è‡ªåŠ¨å¡«å……æ»¡å±å¹•ï¼Œé»˜è®¤ä¸ºæ˜¯
        autoFullHeight: {
            type: Boolean,
            default: u.gc('autoFullHeight', true)
        },
        // æ˜¯å¦ç›‘听列表触摸方向改变,默认为否
        watchTouchDirectionChange: {
            type: Boolean,
            default: u.gc('watchTouchDirectionChange', false)
        },
        // z-paging中布局的单位,默认为rpx
        unit: {
            type: String,
            default: u.gc('unit', 'rpx')
        }
    },
    created() {
        // ç»„件创建时,检测是否开始加载状态
        if (this.createdReload && !this.refresherOnly && this.auto) {
            this._startLoading();
            this.$nextTick(this._preReload);
        }
    },
    mounted() {
        this.active = true;
        this.wxsPropType = u.getTime().toString();
        this.renderJsIgnore;
        if (!this.createdReload && !this.refresherOnly && this.auto) {
            // å¼€å§‹é¢„加载
            u.delay(() => this.$nextTick(this._preReload), 0);
        }
        // å¦‚果开启了列表缓存,在初始化的时候通过缓存数据填充列表数据
        this.finalUseCache && this._setListByLocalCache();
        let delay = 0;
        // #ifdef H5 || MP
        delay = c.delayTime;
        // #endif
        this.$nextTick(() => {
            // åˆå§‹åŒ–systemInfo
            this.systemInfo = u.getSystemInfoSync();
            // åˆå§‹åŒ–z-paging高度
            !this.usePageScroll && this.autoHeight  && this._setAutoHeight();
            // #ifdef MP-KUAISHOU
            this._setFullScrollViewInHeight();
            // #endif
            this.loaded = true;
            u.delay(() => {
                // æ›´æ–°fixed模式下z-paging的布局,主要是更新windowTop、windowBottom
                this.updateFixedLayout();
                // æ›´æ–°ç¼“存中z-paging整个内容容器高度
                this._updateCachedSuperContentHeight();
            });
        })
        // åˆå§‹åŒ–页面滚动模式下slot="top"、slot="bottom"高度
        this.updatePageScrollTopHeight();
        this.updatePageScrollBottomHeight();
        // åˆå§‹åŒ–slot="left"、slot="right"宽度
        this.updateLeftAndRightWidth();
        if (this.finalRefresherEnabled && this.useCustomRefresher) {
            this.$nextTick(() => {
                this.isTouchmoving = true;
            })
        }
        // ç›‘听uni.$emit中全局emit的complete error等事件
        this._onEmit();
        // #ifdef APP-NVUE
        if (!this.isIos && !this.useChatRecordMode) {
            this.nLoadingMoreFixedHeight = true;
        }
        // åœ¨nvue中更新nvue下拉刷新view容器的宽度,而不是写死默认的750rpx,需要考虑列表宽度不是铺满屏幕的情况
        this._nUpdateRefresherWidth();
        // #endif
        // #ifndef APP-NVUE
        // è™šæ‹Ÿåˆ—表模式时,初始化数据
        this.finalUseVirtualList && this._virtualListInit();
        // #endif
        // #ifndef APP-PLUS
        this.$nextTick(() => {
            // éžapp平台中,在通过获取css设置的底部安全区域占位view高度设置bottom距离后,更新页面滚动底部高度
            setTimeout(() => {
                this._getCssSafeAreaInsetBottom(() => this.safeAreaInsetBottom && this.updatePageScrollBottomHeight());
            }, delay)
        })
        // #endif
    },
    destroyed() {
        this._handleUnmounted();
    },
    // #ifdef VUE3
    unmounted() {
        this._handleUnmounted();
    },
    // #endif
    watch: {
        defaultThemeStyle: {
            handler(newVal) {
                if (newVal.length) {
                    this.finalRefresherDefaultStyle = newVal;
                }
            },
            immediate: true
        },
        autoHeight(newVal) {
            this.loaded && !this.usePageScroll && this._setAutoHeight(newVal);
        },
        autoHeightAddition(newVal) {
            this.loaded && !this.usePageScroll && this.autoHeight && this._setAutoHeight(newVal);
        },
    },
    computed: {
        // å½“前z-paging的内置样式
        finalPagingStyle() {
            const pagingStyle = { ...this.pagingStyle };
            if (!this.systemInfo) return pagingStyle;
            const { windowTop, windowBottom } = this;
            if (!this.usePageScroll && this.fixed) {
                if (windowTop && !pagingStyle.top) {
                    pagingStyle.top = windowTop + 'px';
                }
                if (windowBottom && !pagingStyle.bottom) {
                    pagingStyle.bottom = windowBottom + 'px';
                }
            }
            if (this.bgColor.length && !pagingStyle['background']) {
                pagingStyle['background'] = this.bgColor;
            }
            if (this.height.length && !pagingStyle['height']) {
                pagingStyle['height'] = this.height;
            }
            if (this.width.length && !pagingStyle['width']) {
                pagingStyle['width'] = this.width;
            }
            if (this.maxWidth.length && !pagingStyle['max-width']) {
                pagingStyle['max-width'] = this.maxWidth;
                pagingStyle['margin'] = '0 auto';
            }
            return pagingStyle;
        },
        // å½“前z-paging内容的样式
        finalPagingContentStyle() {
            if (this.contentZIndex != 1) {
                this.pagingContentStyle['z-index'] = this.contentZIndex;
                this.pagingContentStyle['position'] = 'relative';
            }
            return this.pagingContentStyle;
        },
        renderJsIgnore() {
            if ((this.usePageScroll && this.useChatRecordMode) || (!this.refresherEnabled && this.scrollable) || !this.useCustomRefresher) {
                this.$nextTick(() => {
                    this.renderPropScrollTop = 10;
                })
            }
            return 0;
        },
        windowHeight() {
            if (!this.systemInfo) return 0;
            return this.systemInfo.windowHeight || 0;
        },
        windowBottom() {
            if (!this.systemInfo) return 0;
            let windowBottom = this.systemInfo.windowBottom || 0;
            // å¦‚果开启底部安全区域适配并且不使用placeholder的形式体现并且不是聊天记录模式(因为聊天记录模式在keyboardHeight计算初已添加了底部安全区域),在windowBottom添加底部安全区域高度
            if (this.safeAreaInsetBottom && !this.useSafeAreaPlaceholder && !this.useChatRecordMode) {
                windowBottom += this.safeAreaBottom;
            }
            return windowBottom;
        },
        isIosAndH5() {
            // #ifndef H5
            return false;
            // #endif
            return this.isIos;
        }
    },
    methods: {
        // å½“前版本号
        getVersion() {
            return `z-paging v${c.version}`;
        },
        // è®¾ç½®nvue List的specialEffects
        setSpecialEffects(args) {
            this.setListSpecialEffects(args);
        },
        // ä¸ŽsetSpecialEffects等效,兼容旧版本
        setListSpecialEffects(args) {
            this.nFixFreezing = args && Object.keys(args).length;
            if (this.isIos) {
                this.privateRefresherEnabled = 0;
            }
            !this.usePageScroll && this.$refs['zp-n-list'].setSpecialEffects(args);
        },
        // #ifdef APP-VUE
        // å½“app长时间进入后台后进入前台,因系统内存管理导致app重新加载时,进行一些适配处理
        _handlePageLaunch() {
            // é¦–次触发不进行处理,只有进入后台后打开app重新加载时才处理
            if (this.pageLaunched) {
                // è§£å†³åœ¨vue3+ios中,app ReLaunch时顶部下拉刷新展示位置向下偏移的问题
                // #ifdef VUE3
                this.refresherThresholdUpdateTag = 1;
                this.$nextTick(() => {
                    this.refresherThresholdUpdateTag = 0;
                })
                // #endif
                // è§£å†³ä½¿ç”¨è™šæ‹Ÿåˆ—表时,app ReLaunch时白屏问题
                this._checkVirtualListScroll();
            }
            this.pageLaunched = true;
        },
        // #endif
        // ä½¿æ‰‹æœºå‘生较短时间的振动(15ms)
        _doVibrateShort() {
            // #ifndef H5
            // #ifdef APP-PLUS
            if (this.isIos) {
                const UISelectionFeedbackGenerator = plus.ios.importClass('UISelectionFeedbackGenerator');
                const feedbackGenerator = new UISelectionFeedbackGenerator();
                feedbackGenerator.init();
                setTimeout(() => {
                    feedbackGenerator.selectionChanged();
                }, 0)
            } else {
                plus.device.vibrate(15);
            }
            // #endif
            // #ifndef APP-PLUS
            uni.vibrateShort();
            // #endif
            // #endif
        },
        // è®¾ç½®z-paging高度
        async _setAutoHeight(shouldFullHeight = true, scrollViewNode = null) {
            const heightKey = 'min-height';
            try {
                if (shouldFullHeight) {
                    // å¦‚果需要铺满全屏,则计算当前全屏可是区域的高度
                    let finalScrollViewNode = scrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
                    let finalScrollBottomNode = await this._getNodeClientRect('.zp-page-bottom');
                    if (finalScrollViewNode) {
                        const scrollViewTop = finalScrollViewNode[0].top;
                        let scrollViewHeight = this.windowHeight - scrollViewTop;
                        scrollViewHeight -= finalScrollBottomNode ? finalScrollBottomNode[0].height : 0;
                        const additionHeight = u.convertToPx(this.autoHeightAddition);
                        // åœ¨æ”¯ä»˜å®å°ç¨‹åºä¸­ï¼Œæ·»åŠ !important会导致min-height失效,因此在支付宝小程序中需要去掉
                        let importantSuffix =  ' !important';
                        // #ifdef MP-ALIPAY
                        importantSuffix = '';
                        // #endif
                        const finalHeight = scrollViewHeight + additionHeight - (this.insideMore ? 1 : 0) + 'px' + importantSuffix;
                        this.$set(this.scrollViewStyle, heightKey, finalHeight);
                        this.$set(this.scrollViewInStyle, heightKey, finalHeight);
                    }
                } else {
                    this.$delete(this.scrollViewStyle, heightKey);
                    this.$delete(this.scrollViewInStyle, heightKey);
                }
            } catch (e) {}
        },
        // #ifdef MP-KUAISHOU
        // è®¾ç½®scroll-view内容器的最小高度等于scroll-view的高度(为了解决在快手小程序中内容较少时scroll-view内容器高度无法铺满scroll-view的问题)
        async _setFullScrollViewInHeight() {
            try {
                // å¦‚果需要铺满全屏,则计算当前全屏可是区域的高度
                const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
                scrollViewNode && this.$set(this.scrollViewInStyle, 'min-height', scrollViewNode[0].height + 'px');
            } catch (e) {}
        },
        // #endif
        // ç»„件销毁后续处理
        _handleUnmounted() {
            this.active = false;
            this._offEmit();
            // å–消监听键盘高度变化事件(H5、百度小程序、抖音小程序、飞书小程序、QQ小程序、快手小程序不支持)
            // #ifndef H5 || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU
            this.useChatRecordMode && uni.offKeyboardHeightChange(this._handleKeyboardHeightChange);
            // #endif
        },
        // è§¦å‘更新是否超出页面状态
        _updateInsideOfPaging() {
            this.insideMore && this.insideOfPaging === true && setTimeout(this.doLoadMore, 200)
        },
        // æ¸…除timeout
        _cleanTimeout(timeout) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            return timeout;
        },
        // æ·»åР免局emit监听
        _onEmit() {
            uni.$on(c.errorUpdateKey, (errorMsg) => {
                if (this.loading) {
                    if (!!errorMsg) {
                        this.customerEmptyViewErrorText = errorMsg;
                    }
                    this.complete(false).catch(() => {});
                }
            })
            uni.$on(c.completeUpdateKey, (data) => {
                setTimeout(() => {
                    if (this.loading) {
                        if (!this.disabledCompleteEmit) {
                            const type = data.type || 'normal';
                            const list = data.list || data;
                            const rule = data.rule;
                            this.fromCompleteEmit = true;
                            switch (type){
                                case 'normal':
                                    this.complete(list);
                                    break;
                                case 'total':
                                    this.completeByTotal(list, rule);
                                    break;
                                case 'nomore':
                                    this.completeByNoMore(list, rule);
                                    break;
                                case 'key':
                                    this.completeByKey(list, rule);
                                    break;
                                default:
                                    break;
                            }
                        } else {
                            this.disabledCompleteEmit = false;
                        }
                    }
                }, 1);
            })
        },
        // é”€æ¯å…¨å±€emit和listener监听
        _offEmit(){
            uni.$off(c.errorUpdateKey);
            uni.$off(c.completeUpdateKey);
        },
    },
};
src/components/z-paging/js/z-paging-mixin.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
// [z-paging]使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法
export default {
    onPullDownRefresh() {
        if (this.isPagingRefNotFound()) return;
        this.$refs.paging.reload().catch(() => {});
    },
    onPageScroll(e) {
        if (this.isPagingRefNotFound()) return;
        this.$refs.paging.updatePageScrollTop(e.scrollTop);
        e.scrollTop < 10 && this.$refs.paging.doChatRecordLoadMore();
    },
    onReachBottom() {
        if (this.isPagingRefNotFound()) return;
        this.$refs.paging.pageReachBottom();
    },
    methods: {
        isPagingRefNotFound() {
            return !this.$refs.paging;
        }
    }
}
src/components/z-paging/js/z-paging-static.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
// [z-paging]公用的静态图片资源
export default {
    base64Arrow: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAD1BMVEVHcExRUVFMTExRUVFRUVE9CdWsAAAABHRSTlMAjjrY9ZnUjwAAAQFJREFUWMPt2MsNgzAMgGEEE1B1gKJmAIRYoCH7z9RCXrabh33iYktcIv35EEg5ZBh07pvxJU6MFSPOSRnjnBUjUsaciRUjMsb4xIoRCWNiYsUInzE5sWKEyxiYWDbyefqHx1zIeiYTk7mQYziTYecxHvEJjwmIT3hMQELCYSISEg4TkZj0mYTEpM8kJCU9JiMp6TEZyUmbAUhO2gxAQNJiIAKSFgMRmNQZhMCkziAEJTUGIyipMRjBSZkhCE7KDEFIUmTeGCHJxWz0zXaE0GTCG8ZFtEaS347r/1fe11YyHYVfubxayfjoHmc0YYwmmmiiiSaaaKLJ7ckyz5ve+dw3Xw2emdwm9xSbAAAAAElFTkSuQmCC',
    base64ArrowWhite: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAElBMVEVHcEz///////////////////+IGTx/AAAABnRSTlMA/dAkXZOhASU/AAABYElEQVRYw+2YwXLCIBCGsdAHWGbyAKZ4zxi9O017rxLf/1UaWFAgA1m8dcpedNSPf/l/Vh0Ya/Wn6hN0JcGvoCqRM4C8VBFiDwBqqNuJKV0rAnCgy3AUqZE57x0iqTL8Br4U3WBf/YWaIlTKfAcELU/h9w72CSVPa3C3OCDvhpHbRp/s2vq4fHhCeiCl2A3m4Qd71DQR257mFBlMcTlbFnFWzNtHxewYEfSiaLS4el8d8nyhmKJd1CF4eOS0keLMAuSxubLBIeIGQW8YHCFFo7EH9+YDcQt9FMZEswTheaNxTHwHT8SZorJjMrEVwo4Zo0U8HSEyZvJMOg4RjnmmRr8nDYeIz3OMkbfE/QhBo+U9RnZJxjGCRh/WKmHEMWLNkfPKsGh/CWJk1JjG0kcuJggTt34VDP8aWAFhp4nybVb5+9qQhjSkIQ1pSEMa8k+Q5U9rV3dF8MpFBK+/7miVq1/HZ2qmo9D+pAAAAABJRU5ErkJggg==',
    base64Flower: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAKlBMVEVHcEzDw8Ovr6+pqamUlJTCwsKenp61tbWxsbGysrLNzc2bm5u5ubmjo6MpovhuAAAACnRSTlMA/P79/sHDhiZS0DxZowAABBBJREFUWMPtl89rE0EUx7ctTXatB3MI1SWnDbUKPUgXqh4ED8Uf7KUVSm3ooVSpSii0Fn/gD4j4o+APiEoVmos9FO2celiqZVgwgaKHPQiCCkv+F99kM7Ozm5kxq1dfD91k9pPve9/3ZjbRNHHok/mKli4eIPNgSuRObuN9SqSEzM20iGnm0yIbqCuV7NSSSIV7uyPM6JMBYdeTOanh/QihJYZsUCSby+VkMj2AvOt0rAeQAwqE3lfKMZVlQCZk1QOCKkkVPadITCfIRNKxfoJI5+0OIFtJx14CMSg1mRSDko7VAfksRQzEbGYqxOJcVTWMCH2I1/IACNW0PWU2M8cmAVHtnH5mM1VRWtwKZjOd5JbF6s1IbaYqaotjNlPHgDAnlAizubTR6ovMYn052g/U5qcmOpi0WL8xTS/3IfSet5m8MEr5ajjF5le6dq/OJpobrdY0t3i9QgefWrxW9/1BLhk0E9m8FeUMhhXal499iD0eQRfDF+ts/tttORRerfp+oV7f4xJj82iUYm1Yzod+ZQEAlS/8mMBwKebVmCVp1f0JLS6zKd17+iwRKTARVg2SHtz3iEbBH+Q+U28zW2Jiza8Tjb1YFoYZMsJyjDqp3M9XBQdSdPLFdxEpvOB37JrHcmR/y9+LgoTlCFGZEa2sc6d4PGlweEa2JSVPoVm+IfGG3ZL037iV9oH+P+Jxc4HGVflNq1M0pivao/EopO4b/ojVCP9GjmiXOeS0DOn1o/iiccT4ORnyvBGF3yUywkQajW4Ti0SGuiy/wVSg/L8w+X/8Q+hvUx8Xd90z4oV5a1i88MbFWHz0WZZ1UrTwBGPX3Rat9AFiXRMRjoMdIdJLEOt2h7jrYOzgOamKZSWSNspOS0X8SAqRYmxRL7sg4eLzYmNehcxh3uoyud/BH2Udux4ywxFTc1xC7Mgf4vMhc5S+kSH3Y7yj+qpwIWSoPTVCOOPVthGx9FbGqrwFw6wSFxJr+17zeKcztt3u+2roAEVgUjDd+AHGuxHy2rZHaa8JMkTHEeyi85ANPO9j9BVuBRD2FY5LDMo/Sz/2hReqGIs/KiFin+CsPsYO/yvM3jL2vE8EbX7/Bf8ejtr2GLN65bioAdgLd8Bis/mD5GmP2qeqyo2ZwQEOtAjRIDH7mBKpUcMoApbZJ5UIxkEwxyMZyMxW/uKFvHCFR3SSmerHyDNQ2dF4JG6zIMpBgLfjSF9x1D6smFcYnGApjmSLICO3ecCDWrQ48geba9DI3STy2i7ax6WIB62fSyIZIiO3GFQqSURp8wCo7GhJBGwuSovJBNjb7kT6FPVnIa9qJ2Ko+l9mefGIdinaMp0yC1URYiwsdfNE45EuA5Cx9EhalfvN5s+UyItm81vaB3p4joniN+SCP7Qc1hblAAAAAElFTkSuQmCC',
    base64FlowerWhite: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAElBMVEX///9HcEz///////////////84chYNAAAABnRSTlP/AGzCOYZj5g1nAAACfklEQVRYw+2YTVPDIBCGtza9Jw25a0bvcax30o73OOr//yvma2F3YWlpPTijXNpAHrK8LLALVPFium2vNIFSbwGKTGQA2GUiHcD29yDNy3sMIdUBQl7r2H8mOEVqAHgPkYZUS6Qc2zYhQqtjyDZEximCZwWZLIBeIgYShs2NzxKpSUehYpMJhURGb+O+w5BpMCAREKPnCDHbIY20SzhM5yxziAXpOiBXydrekT9i5XDEq4NIIHHgyU5mRGqviII4mREJJA4QJzMiILwlRJzpKxJKvCBm8OsBBbLux0tsPl4RKYm5aPu6jw1U4mGxEUR9g8M1PcqBEp/WJliNgYOXueBzS4jZSIcgY5lCtevgDSgyzE+rAfuOTQMq0yzvoGH18qju27Mayzs4fPyMziCx81NJa5RNfW7vPYK9KOfDiVkBxFHG8hAj9txuoBuSWORsFfkpBf7xKFLSeaOefEojh5jz22DJEqMP8fUyaKdQx+RnG+yXMpe8Aars8ueR1pVH/bW3FyyvPRw90upLDHwpgBDtg4aUBNkxRLXMAi03IhcZtr1m+FeI/O/JNyDmmL1djLOauSlNflBpW18RQ2bPqXI22MXXEk75KRHTnkPkYbESbdKP2ZFk0r5sIwffAjy1lx+vx7NLjB6/E7Jfv5ERKhzpN0w8IDE8IGFDv5dhz10s7GFiXRZcUeLCEG5P5nDq9k4PFDcoMpE3GY4OuxuCXhmuyNB6k0RsLIAvqp9NE5r8ZCSS8gxnUp7ODdYhZTqxuiJ9uyJJtPmpqJ7wVj+XVieS903iViHziqAhchLEJAyb7jWU647EpUofQ0ziUuXXXhDddtlllSwjgSQu7r4BRWhQqfDPMVwAAAAASUVORK5CYII=',
    base64Success: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAElBMVEVRUVFHcExTU1NRUVFRUVFRUVFOSlSUAAAABnRSTlP/AI6+VySB3ZENAAACcElEQVRYw+2YyYKCMAyGI8hdpdxdZu7gcpdZ7jL6/s8yYheSNi0aPdqbwOffpGmaFOYPD3gj4bisN7vddv17N/JVgxn5x12IWgIaWTuO/IE3PseQbwjGPo2cgRmHFLJwdm/X643zwiqOKPPJ1nj3sjEP2iiifZWj5bhopSyGaEO2HX5fbQJzwJ+W7x/jw5ZFjsEU0PMph9xE8i5EqprKALW95eJQURkgzw98uJ/JvwGecR7bIjWWsUgVrrIfFZ2HlLy3sKETD1mmRLRMRhGVssRa0xJkdn3SpJBymBkM8+pSSDXMDNyDaToVHd2fgpNt0sjwiUZO19+jGQ+gQEg9Oq+bufmAVGihomNmjQG7UG3020vrlm7lkFnKFGU3kZ0KGAdmKe821pipQ+qEKcrZeTL2g5FsUks4cStjEZWwXg0b0n4GxmEpkWwIs5VBynjgK7xZaz1/0D7OxkVuLpsY5BQNFyLS84VBjjbg0iL2r2EQHBOxBhikuUOkdxODVF1cxHoWtPPsiyXO455Iv34hssCO8EV4ZIYTjS8SR4qYSHRiTiYQ4ZFbHi0iIhhBTi6dTCgSWRcnw4h4yGTuyTAiOGBIWGoZTgSHJQl+LcOJ4OCnW6yX2bMnJ9pidCOXtkTkTrIGpYuOynAiOF14SamMiOCk5Ke+mq8BcOrrvym8d0zKIQnWT+M1WwOQNO4fFiWb18hhERxJPx2fblbPHHyC41VyiAtKBUFBIih7JMWVoIQTFIr3lKPN80WvoLSWFPC653ioTZA0I0FrQ7qU6asaK0H7JmkSJa2ooOGVtNUsc3j9FYHkIkJy3SG6VHnfXKXGP9t4N9Q4Ye98AAAAAElFTkSuQmCC',
    base64SuccessWhite: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAGFBMVEVHcEz///////////////////////////8dS1W+AAAAB3RSTlMAiVYk6KvDHLfaegAAAo1JREFUWMPtWEtzmzAQNhCTq910ytXpiyvxTNOr60zrayepx9d02gnX4sTm7xcEiJX2gdnkGJ1A4tOnfWqXyeR1vMRYzrcPD9v5h5MBl3/Ldvx4cxIg/FWC8X0xjLjalM54uhhCfCrRuJURX0pi3EmIqZV7O59vrRZmguStHL9b7S7ftfLwOtiZDw7AHMtmquAQ12b5Wwbnordm8g9zLLO49qc/m2n6aKnhwPOGZ08hAiNHhheiHae1lOUPGZpQkPKa3q0mOUjaRzSRaGUjpy/mmWSwySSpllcEteBKAT52KEnSbblA51pJEPxBQoiH1FP4E3s5+FJv07h6/ylD6ui7B+9fq/ehrFB98ghec9EoVtyjK8pqCHLmCBOwMWSCeWFNN4MbPAk55NhsvoFHSSVR0k5TCTTEzlUGcqV/nVp7n9oIVkmtaqbAEqEgfdgHJPwsEAyZ9r4VAZXFjpEwyaw3+H2v42KYxKhs1XvY/gSSGv+IHyUSuHXCeZhLAgVI3EjgSGo1Fb3xO0tGGU9S2/KAIbtjxpJASG73qox6w5LUq0cEOa+iIONIWIilQSQ0pPa2jgaRQAgQP7c0mITRWGxpMAmEQFN2NAQJNCV0mI6GIIEO47hlQ0ORQLd0nL+hoUjg1m6I1TRr8uYEAriBHLcVFQ5UEMiBe3XkTBEG04WXlGKGxPnMS305XQPA1Ocn2JiuAZwE66fxnKwBnDTuXxZTMq85lwW6kt5ndLqZPefiU1yvmktcUSooChJF2aMprhQlnKJQ5FxRKkcVRa+itNYU8Io2oVkY14w0NMWYlqft91Bj9VHq+ca3b43BxjWJmla0sfKohlfTVpPN+93L/yLQ/IjQ/O5Q/VR5HdL4D7mlxmjwVdELAAAAAElFTkSuQmCC',
    base64Empty: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQAAALeGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDIgNzkuMTY0NDg4LCAyMDIwLzA3LzEwLTIyOjA2OjUzICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAyLTIyVDIxOjIxOjQ1KzA4OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDI0LTAxLTEzVDE5OjA5OjQwKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wMS0xM1QxOTowOTo0MCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZWQwMWYzNWQtOWRjOC00MDBiLWEyMmQtNjM5OGZiNzVhNGRiIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDhlMzQ3ZmEtMDY2My1jYTRiLTgzNTctNTk4YjBkNGIzOTU2IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZDA4MDI4MDItMzUyYS04NTRhLTkxYjctNmRlNmQ1MmViM2QwIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHRpZmY6T3JpZW50YXRpb249IjEiIHRpZmY6WFJlc29sdXRpb249IjMwMDAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjMwMDAwMDAvMTAwMDAiIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiIGV4aWY6Q29sb3JTcGFjZT0iMSIgZXhpZjpQaXhlbFhEaW1lbnNpb249IjMwMCIgZXhpZjpQaXhlbFlEaW1lbnNpb249IjMwMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZDA4MDI4MDItMzUyYS04NTRhLTkxYjctNmRlNmQ1MmViM2QwIiBzdEV2dDp3aGVuPSIyMDIyLTAyLTIyVDIxOjIxOjQ1KzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQwNjg2NzJkLWY5NDMtOTU0Mi1iMDBiLTVlMDExNmE1NmIzZSIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0xM1QxMDoyNjoxNiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphYmJkZmUyZC0xY2Q2LTJiNDgtYjUyNS05YzlhZjdlNjA4NDMiIHN0RXZ0OndoZW49IjIwMjQtMDEtMTNUMTE6MjM6NDArMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YTQ5MjM5MDAtNDhiZC03YTQ1LWI4NGItYmVlZTVjOWUxYTM1IiBzdEV2dDp3aGVuPSIyMDI0LTAxLTEzVDExOjIzOjQwKzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmVkMDFmMzVkLTlkYzgtNDAwYi1hMjJkLTYzOThmYjc1YTRkYiIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0xM1QxOTowOTo0MCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIyLjAgKE1hY2ludG9zaCkiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmFiYmRmZTJkLTFjZDYtMmI0OC1iNTI1LTljOWFmN2U2MDg0MyIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjM2ZGQ4NTQxLWQ0MWEtYmY0Yy1iZjA3LWNmNjZhNjZhMDg2MSIgc3RSZWY6b3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmQwODAyODAyLTM1MmEtODU0YS05MWI3LTZkZTZkNTJlYjNkMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pm30U/gAAAAJcEhZcwAALiMAAC4jAXilP3YAAAA/UExURUdwTODg4O3t7e7u7unp6d7e3uTk5M/Pz8nJyePj4+jo6Pj4+MrKyszMzO7u7unp6fb29vLy8vr6+v7+/sHBweag3xAAAAAOdFJOUwAxia5pF0n+/vzX3KbULQ2DYQAACG1JREFUeNrtm4l2o7gShi20IWFrAd7/WUc7EosDWKZ976Hc7WTmdMKXv0qlqpLyeNx222233Xbbbbfddtv/mOHn8xexSNsiRH5PrbFtW4p+DetpsF4v8Gs+HA3WEwOAfwzriYxaLTVsP8X1QK0z+vqQCzewYogi60aL9SEX5oyxphYVCFTGjfSJCTmN1jBruN5KTGCUS8bhySQGHRaohmW4glwtldbOeYJYKlgvbyUuA8aFFEKc++aIM4hrRnyiMnIZKq1PrihcM3GNKboMF1Naa9X9+8T1KrxIlVbGjv3cAEHOYYMqqgUsVuJqqehV3+sjDwB+DTJp0lYtMCyZpxqjF4e+74+sRcQSFZO8UonUSEFzuUY+DKo59A2kZDatGCjzCauy/2AmhSyCq0WHEj0KTNJDmVeNhErMt1Q8W4xti4/FwMJ4jaxl05TKFiNtD3kBGrHnhiph9V0eXQc6DkyE2xX830AlKshFTErXeuCZXK/9m41wFsGSfZ4lcGeyZ98PrylJ7MWCojQZ3qSukL2QslgdngqJnTEPdTJhXvbNBoR/+7wabIxWduN/Ja5dWEivm4XSZ2uQckNzmRlHrn2lc6eiafvS4V2Hd12tesau8toZW0CtWoZYb9t+OqxdCYKYjVPF16pVbILIy/gR7MVaWMHYPCoa2VkzkX4Iry2rirXbumGyAjGC1h62YLw6ApsNKZph3fpIWHt08JovRWD62sejpXhTrhWrPpl6zZ6PW2oTG5ltlvgtF6weNYCWKeJJSfg4W6PNJlj3sVZgOXV4lc8n4RlkMTLEBDVoYc3nI09kpyzzfgWsjyzBZSNDKF2/wjh+sxYvn8Y1scxlfLF9T1RBO3wVHsnq8Fk4oGkEh/0KJPSa8T2CeWE5X9BPmgLsaRIGeNL2kshCsWoLBmdPJW5Wbz1ndAKUXjPwxXYAUpSV3fy5BJg1aa1tyVXHHMgVH31ewDVrleHr9XqC684SUF4mecR3+wW5SC2QNvxUizRv98mLDhPgYiMDb+v8g0OADxqxcnf9w01mZYJF0fUVP5LcdswbsMmy1DVs5PlE5NpNiTR8M8qAWZkOy6aN13VcoOF2/s3xn3Mes8Xza05tgR/BuNz69nlNzMR0fH45p+G4R9oxh2mKt9MF4J7K/lvWUojwF5nCgCpuRUptnZMQ3au0nSo2UsHgV3xpmeLYzGml3ZFBBzYGPCpOQRwXs1/GG1J74dlZc6JKUOtjBAz9XjVxucGWHbZVJDPJQGYDRl1Qmf1ovk2Sbghb6MQlnF7mBzM1bgOqJAPpoOQaVe+4Skcit3uqHMyG/Sh1rHNN0gAfM0nnPrmulfLVBSm20TSZSdWa0LJl2ukVyE4vTYCgP3uQkwv1TKtQWgxDzBSg80OQjCs4klKvuUzHLCfIbDKIE/S5VIGqD1iD2819pkAqTWdmeina+oZABi7X5B1MGoTJqJSchuk6JNHcgUPAcsVFk0+N0oDN68Vo7FQSmCXjx46OEtUk1lpY2ZFQGr/AcpqVato4wPUD+RhfAeyQI5sJ6l2sDwnKqNFSJvpiyJbFl3kTOjZ2ievwCR7hkUoWeV2vOLAXvB39AJoyqYa81A5cvaAidXYTFTycKDBcalVK5f3XS89kzLVl9txfL+K+p6NUnitz5KkKm7D3DrRPNq4bk7l20aFRppNilmuQI+uzTtj9wPBkTsVwM7HbJ5pwGgujyRyZDzQLNoiRFluRtQ+GzEguqRxUL+ZMFqulMzIfaP3ARj2k/txB8c+2HyjmDizCaVWtNoE5MvMlKs/4VQ7HUJZCrU6qCKcNJ2aSWUZhJZu4VI0LB4CHFdj77DRuGi28WKAxoRyZyzGVrmc0jmk1nP5QaxZo1puqq1YIAqgZb8e/rABZJWNCNxV7DSTpOO7Aail9J9nYHtua/4ouE/aS0X1qtXQzwGx+rnbi2vhF/TfZG52oc6DPo1WCi3RTDnRk7TEntoEp38gg+DjYs2opkR3JW5EpL9rU0XSK5/6LOTAVS+72x7pm60zSf5HMdldjhzJqw1FRcxXdS3ZNZp0s92FiyluUvBPoD9ynZNkBiu2NF11ofnlnQbZgKqvusj9R/f6DOzgVsahbNlXxlsxU8y7qrbTupitRyxFBKG6H3aEPUqj7YrzAymq41FXlZLlO4WLbvG2Kg4vYB+wPfWS2B5Rq8TW9ROpAZbiF6MmCTsx1NLLsx7NOoOiZup2CNbZ36xc96ErcxzuILGrmmFhimjtwKo/yTm7feTVwB61IzbnW4967Kt3cDDotGt8JKrTiUyO3Uy2PZZt9tapXEfXhWmTgcoB+JchFWsiCKvYnhmn/tKuJDbgly897FnFfkE1rQLKy810OU7xW3bEJHCD5gERtuTGuxoJqA6qI9TNMa6MbvZomsiubbPYx78YXDaaRqqsyqfSaLZdjYGHLu65rDgydXCWm1P5EvcQ828f9pcBapTILSMv1nZCAc0WzFIFsGfUi/kmAxc6cFqDSYuPSMIbs1OVrwITTQM9HVRFJ5JL56qcoFzzT1uVcd2v9jFw8BHlcWtmEI86hp5Dy/zOlK8cUp/rVseRUBqawz6kmAcPLM9l5m8h4V53Iz/2mFJaTCvF8JbsMvPjU/7crbUXart0v4WyE0LnDPcAX95Knj4VUE8HCdNdUP8BDcOXKdPl4uSWbh4LfOV0HDdfipOmu+eIRrDsNPkIT7np/8ZAzVdOd1u8wHIqeXt8VqtgiO50ePeNaGG+uO9rHiKdL71pnIun8jxEKXv2r2HYBzO/mz96vFKoMM5WLk7tQXS9U5kwCu5lk7n6++kdCFWRaTUzm0/5fClWGWTrM/AGhCrJO/ZBQhTPFLwmV7ebgcdttt91222233Xbbbf+H9h+2WEtdHVinLAAAAABJRU5ErkJggg==',
    base64Error: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQAAALeGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDIgNzkuMTY0NDg4LCAyMDIwLzA3LzEwLTIyOjA2OjUzICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAyLTIyVDIxOjIxOjQ1KzA4OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDI0LTAxLTEzVDE5OjEwOjEwKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wMS0xM1QxOToxMDoxMCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTQ3NTExNjAtZDY5MC00ZTkzLWFhNGUtNGMwYTViNGU1ZGFjIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YzRiNzlkYWMtZTJmYS1iNzQ0LWIxM2ItOWU1N2VjMDhhM2YwIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZDA4MDI4MDItMzUyYS04NTRhLTkxYjctNmRlNmQ1MmViM2QwIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHRpZmY6T3JpZW50YXRpb249IjEiIHRpZmY6WFJlc29sdXRpb249IjMwMDAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjMwMDAwMDAvMTAwMDAiIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiIGV4aWY6Q29sb3JTcGFjZT0iMSIgZXhpZjpQaXhlbFhEaW1lbnNpb249IjMwMCIgZXhpZjpQaXhlbFlEaW1lbnNpb249IjMwMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZDA4MDI4MDItMzUyYS04NTRhLTkxYjctNmRlNmQ1MmViM2QwIiBzdEV2dDp3aGVuPSIyMDIyLTAyLTIyVDIxOjIxOjQ1KzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQwNjg2NzJkLWY5NDMtOTU0Mi1iMDBiLTVlMDExNmE1NmIzZSIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0xM1QxMDoyNjoxNiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpjZjk1NTE1OC04MjFiLTA4NDUtYWJmNS05YTE1NGM1ZTY4NjEiIHN0RXZ0OndoZW49IjIwMjQtMDEtMTNUMTE6MDQ6MDQrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZGM1Y2IyNWItZDZlNC0yZjQ2LTgyODQtZmUwOTNlY2M2ZTkxIiBzdEV2dDp3aGVuPSIyMDI0LTAxLTEzVDExOjA0OjA0KzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjE0NzUxMTYwLWQ2OTAtNGU5My1hYTRlLTRjMGE1YjRlNWRhYyIgc3RFdnQ6d2hlbj0iMjAyNC0wMS0xM1QxOToxMDoxMCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIyLjAgKE1hY2ludG9zaCkiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmNmOTU1MTU4LTgyMWItMDg0NS1hYmY1LTlhMTU0YzVlNjg2MSIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjM2ZGQ4NTQxLWQ0MWEtYmY0Yy1iZjA3LWNmNjZhNjZhMDg2MSIgc3RSZWY6b3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmQwODAyODAyLTM1MmEtODU0YS05MWI3LTZkZTZkNTJlYjNkMCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Ph2LDQsAAAAJcEhZcwAACxMAAAsTAQCanBgAAAA5UExURUdwTNra2s7Ozq2tre3t7dPT087OzuPj4+3t7dbW1u/v79bW1vz8/MrKytDQ0Nzc3MPDw/X19bi4uMZQDnEAAAAKdFJOUwBqEPywotz+wzqApqiTAAAHW0lEQVR42u1b25akIAwcbx2UFoj//7HLTQVBRcSZfTDnbM/uTl/KSlEkwf75eeONN95444033njjjTduR9/0/yOsbqoevObL7101tYX1HFs9QFtfZalRP+rpQVgdAFx990ZnT8L6eZItUl99jeGpf1DxdV/VP9fV1f/PFlF1bYHoVFSRC60IyVjrFRnuB8IoxpExSrstsErKHpJw1eqybNLbAQvAYkKjUrjoBgKRqAaeIjG5+qaps6hKcMWmcdSwqAJWBbAgCZZaIYbsqggqqlHNbFFa5yVR4jKvrKEErOEjNCqNSwHrfE8lpLsod/u+cOPPMPBJ+Gz5dM0cXNgclre+pSxhYI1WW5Tf9ENSMIdLCiWs6q9hwQprBVYKFqyPlx4WtoSvrT9lC/wkGt8qlkQooC3hi6sgW3Bb8gtdpSV/za/mn49pC0oYhONbfyd5hzDLFivKFpTS1gKM0we0tQCEncfgQn7Rt+DC/299i1MSRJcBC0r7VviG5KZvwV5WIUobxHyrJKy8VRjXVgFYsPu5kOtbxdhycCDuihziXVLoW7xwEiUmDgd544B46luWLW+nugMLB2BimmC3cxTNxCDg8xFtuUSNqoFsDKzY8psa+XtBNWXr74N6qxwsS5T6VL5robKl10+ZRu5S9qBvUYuJwVHzjwjrE3G33qKh+WXBgmkmCvHYquTvZ8oo7rLFA4PJgYW0MdePIRQIGUPNbSMw5lubJMKtJI6+Wk6cVFMmACO+VVryeL7ZgI8MhwS2fnNPPK0geHBRd11eJSiyL4KjrL2umm1XIpRii1MKB/mU/iCZwF+pt5z3UJ7UiF3nQqadAXC3T3xEW2IyuDBe3yDTe0+A64it2WTyYSGVHymUI/EduvSWKJ80Dtv2NbYSoQxbMkVC7yzNGIWFvDF7gRD79RYrWW/BDGti4wwLtgvO7gWKUZ8Mt94qX8vLJE70+xVNwzDm9ghNM+FX7p/jlZUId2HJD+Tf79hMe3WNrAK/30E+C8/6xOCqbqxE5JNMYrNbnaLUvJAewfCg8zF0Ba/tbviWLvPYfsGFA1PVD8ZdnjlVc/DS/o7LK4NHjOjKKbfCTSCo5XmwKbaZM4jlc9NGEYd9Ijd0QS5ZGaOR2O+DPlGyRb2nXZzgnI1GdFWF+0gh3ifyTRqvzpXI2eElk58FeHziCF5hY+hSMV9Ge/mohUTGuQ4vzHYe8bW5sNdFQ58St22Vcf5zzJbtcGT4iYQ7iz8dFuxoWRYMjAM7KCnypHOTLSqdUwYIFpndOD/6B2FBzNQxYmW/zxYE4j8yLHga1s2Rbm/O5PXtGcuNDIW1dTj5hpjGsO+7z2Kk9NP1JWDlnWKAM4H6zCUNM05KyVPHBclYzUbgjE3N3tP2JWHBmbqD4GLeCs2jhMT13lMVljwcEbetwZgtHUxVQ21ho3fE7inf2s8vzMWq0EWpfOBg5hcDSGwaF2+LaysRIzNFqRgBv2sMhi/Ix0WiW8rBKNBv4ExBI7eorx9ANazsPCb5FkSNH+Reacos+AYxaFzX76KMH65c8ytzZ40YvpFAqtgC/otn1eCmMI5K8yVRQVVwq3aVtU+jJktwjyP7x+BKv8vtoH098vXYSJcrWGJcAW11r8WVRxe5vgcuFbXqwnaEZejS6mrLwYKUg1ch2RJswTFYgMOwoau+AQsSp/FuDhVZi7J402ifgGla/GJIzGLYG5H4rnKMCUydL9wcsmZSuPikR2QmjQbWqaV2ob2RdMvaLEvFlRiXpYeTwqVOtMZF+qi0dS4uEjJKMvWuYK3S0jHZwaq7BylYp/O2uu3q04lNqudLWEJQd/3paTBz12IaLIPtzE5P1AUuW9TB8NVzaG9/TIfV+eXsWeezz6HWlptEbo4SIAeWur/Y/RZC/gmZTiLzUY2j5ct6fjKsFvxqgyQxE9sbmfYtnJMIciEKo6+FL0wziJmtkzspIcUl0PgWrL7VCKP7hl61U4WLeN+7Ieli2vZhmq0VgjDOgIyhJ62sSpDkWNZa1wiB8WoLlxzy29XpGVPgn1ut5VYcGyRLK7OCiJaDYMrAneJUkZWdw0yDgNm5nDowqLc0Kp581FO7QS4pC9S/YRW9xkVdNOj0ZHCp9anEZw3VEK/fopiDrkMObkcdJtT1g6+uzQ60bIdUPztdWZWy53m+v/zFYPOGHO4AZsalmtJNkyHrCAx1RXX7mt5g1L1pDezpkXv8wJwpVRSSaf2c26Y0rrXXxyWBptu/ovdak+VhkqjGBZUdvKygqANKA/MqZ/36kcGwFn90RnWp66ksKuHgitLFY8BU+F2ZvqpxpMY9qR3YwOUJ12fc0KUHVKdswcKXuwetErCnwvMKuXxfc/3RVJ2yFc+iosQd3X+WGSVz1UiuN2J156FyVyHbsOUp3krezaPUT/VxXqdfwvknb/Zgp+idTxTbrkLqYuKreRnhy65Gf4W0NsDoYiqf6uZsvr8V9eo6XWc5+3TVf/3N1TfeeOONN95444033njjjTfeSI1/IeOYOeO4fGAAAAAASUVORK5CYII=',
    base64BackToTop: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIBAMAAABfdrOtAAAAElBMVEVRUVH+/v5HcEyZmZlRUVFRUVGm1ByOAAAABnRSTlPMzADMTZAJBBGsAAAEnElEQVR42t2cS27jMAyGf7/2U+QCQeDsbeQCgZDujaC5/1UmkzaJn+JDFGcw3LdfflKibJkkDnxrL7dbg7sNt6+L4O8OYBM+B0ys+QrGkHZG+OEEQ8g6go8Bx1GIGMdpNOQyIG6XdMgnSPtKhLQDGEZFBgYMkhKFtGBb0EIEjDgFRowoBVaMGAWpMedEfxMiZtwpUsgZCqtlkCNUdpVAWigtCCCDFtLwIWeoreZCWiRYYEKGFEjDg+yRZCUH0iLRAgNyToXUNCRZyMqWhGnUN2IPm3wSlwJ7IUspyCBkIQUZhCykIIeQuRTkEDKXAuM9srrtYbrZN7Y98giZSoFd+t1OxmMITG0dcrSFXFchZ1tIvQZpYWxhBbK3hpQrkMEa0iwh5t4a+QvZvDXyF7J5a+Qv5PPW21/I5623v5DPW29/IaO3Xv5Clrw1y1/Ikrdm+Qs5svw83yNnSJ5BQb4F/F7EIEJSnThGBAXxkFQfLOviQUE8JAUPsosHBfGQfDAtHhREQ1JxIV00KIgmrnRI84S0yAd5BAXxxJUck0f6Qnwr9qmr6xF5xLMjcwn/iudIEAdWnyjkEXlQKZiRVzoqRyLbgeUKKR8Q4alY7cSnoxzSf2ggsqehKr6YVpcXpOd7H93f60cKhOd7Re2LteUF4eLqiVS1mr0ge4io6C2+soaFkJ7MuuuQs1yITEp9hwwKISIpzR2iESKSIoT0rLNwuVHQqoSIpAQJpGce60vIUSdEIuUqgPTsJ5QFZK8UIpBS8iG94GFrDjlrhfCl8CG96Llxmle4kEr6vKWBPIVo9kqDQSRk9/3cWoikcCFPAd33v4dIChPyEvLzBA6RlEYWke4JEUnhKXkLeUEKxRHJFfKCQHGucIW8IdZSRkLeEGMpYyEjiK2UsZARxFTKRMgYYillImQMMZQyFTKB2EmZCplAuFLIHT8TMoWwpQwiIVMIUwqpZP5bp5CCvCTiQKr5f5lCQN+tPCBn2ZvVDFJwIDUP0m1BYAfZYRNSsCB7BqTbhoARePIxtZ9tgwWkoJcwCalmv3MBAemtO4R6dah2HaKQqj8Zvp9sQDjvJ21+SPCBHPJDDk6QITekEV7gqCC19CpKAym9IMfckKv4olMBCeIrWwVEfvkshzQekO9r9P1/ALk+IG1eSPCDiCJfyG+FyU+A6ZCa/piZDinpz7LpkCv5gdkAEshP5emQhv7onw6pGeULyZCSUYiRDAmMkpJkCKs4JhFSq8p8hJBSVbAkhARV6ZUQoisik0FqXTmcDHLVFfbJIEFXoiiCNMpiSxGkVJaNiiBBWQArgTTaUl4JpNQWJUsgQVteXQg+AKkLxQWFGKW+5J2+eVp4S168X3CF1CltCKdTJ8lb84YK2bUBO+wZW0Pqv9nk4tKu49N45NJC5dMM5tLW5tOg59Jq6NM06dL+abFXwr/RkuvTXJwae1abtE/Dt0/ruksTvs84AZ/BCC4jHnyGVfiM3VBQFANEXEah+Ax18RlP4zNox2dkkM/wI58xTn8yDCXGYCDV3W5RGSajtXyGhG1jbpbjzpwGt/0MJft8jqC7iUbQ/QZaxdnKqcIftwAAAABJRU5ErkJggg==',
}
src/components/z-paging/js/z-paging-utils.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,302 @@
// [z-paging]工具类
import zLocalConfig from '../config/index'
import c from './z-paging-constant'
const storageKey = 'Z-PAGING-REFRESHER-TIME-STORAGE-KEY';
let config = null;
let configLoaded = false;
let cachedSystemInfo = null;
const timeoutMap = {};
// èŽ·å–é»˜è®¤é…ç½®ä¿¡æ¯
function gc(key, defaultValue) {
    // è¿™é‡Œreturn一个函数以解决在vue3+appvue中,props默认配置读取在main.js之前执行导致uni.$zp全局配置无效的问题。相当于props的default中传入一个带有返回值的函数
    return () => {
        // å¤„理z-paging全局配置
        _handleDefaultConfig();
        // å¦‚果全局配置不存在,则返回默认值
        if (!config) return defaultValue;
        const value = config[key];
        // å¦‚果全局配置存在但对应的配置项不存在,则返回默认值;反之返回配置项
        return value === undefined ? defaultValue : value;
    };
}
// èŽ·å–æœ€ç»ˆçš„touch位置
function getTouch(e) {
    let touch = null;
    if (e.touches && e.touches.length) {
        touch = e.touches[0];
    } else if (e.changedTouches && e.changedTouches.length) {
        touch = e.changedTouches[0];
    } else if (e.datail && e.datail != {}) {
        touch = e.datail;
    } else {
        return { touchX: 0, touchY: 0 }
    }
    return {
        touchX: touch.clientX,
        touchY: touch.clientY
    };
}
// åˆ¤æ–­å½“前手势是否在z-paging内触发
function getTouchFromZPaging(target) {
    if (target && target.tagName && target.tagName !== 'BODY' && target.tagName !== 'UNI-PAGE-BODY') {
        const classList = target.classList;
        if (classList && classList.contains('z-paging-content')) {
            // æ­¤å¤„额外记录当前z-paging是否是页面滚动、是否滚动到了顶部、是否是聊天记录模式以传给renderjs。避免不同z-paging组件renderjs内部判断数据互相影响导致的各种问题
            return {
                isFromZp: true,
                isPageScroll: classList.contains('z-paging-content-page'),
                isReachedTop: classList.contains('z-paging-reached-top'),
                isUseChatRecordMode: classList.contains('z-paging-use-chat-record-mode')
            };
        } else {
            return getTouchFromZPaging(target.parentNode);
        }
    } else {
        return { isFromZp: false };
    }
}
// é€’归获取z-paging所在的parent,如果查找不到则返回null
function getParent(parent) {
    if (!parent) return null;
    if (parent.$refs.paging) return parent;
    return getParent(parent.$parent);
}
// æ‰“印错误信息
function consoleErr(err) {
    console.error(`[z-paging]${err}`);
}
// å»¶æ—¶æ“ä½œï¼Œå¦‚æžœkey存在,调用时清除对应key之前的延时操作
function delay(callback, ms = c.delayTime, key) {
    const timeout = setTimeout(callback, ms);;
    if (!!key) {
        timeoutMap[key] && clearTimeout(timeoutMap[key]);
        timeoutMap[key] = timeout;
    }
    return timeout;
}
// è®¾ç½®ä¸‹æ‹‰åˆ·æ–°æ—¶é—´
function setRefesrherTime(time, key) {
    const datas = getRefesrherTime() || {};
    datas[key] = time;
    uni.setStorageSync(storageKey, datas);
}
// èŽ·å–ä¸‹æ‹‰åˆ·æ–°æ—¶é—´
function getRefesrherTime() {
    return uni.getStorageSync(storageKey);
}
// é€šè¿‡ä¸‹æ‹‰åˆ·æ–°æ ‡è¯†key获取下拉刷新时间
function getRefesrherTimeByKey(key) {
    const datas = getRefesrherTime();
    return datas && datas[key] ? datas[key] : null;
}
// é€šè¿‡ä¸‹æ‹‰åˆ·æ–°æ ‡è¯†key获取下拉刷新时间(格式化之后)
function getRefesrherFormatTimeByKey(key, textMap) {
    const time = getRefesrherTimeByKey(key);
    const timeText = time ? _timeFormat(time, textMap) : textMap.none;
    return `${textMap.title}${timeText}`;
}
// å°†æ–‡æœ¬çš„px或者rpx转为px的值
function convertToPx(text) {
    const dataType = Object.prototype.toString.call(text);
    if (dataType === '[object Number]') return text;
    let isRpx = false;
    if (text.indexOf('rpx') !== -1 || text.indexOf('upx') !== -1) {
        text = text.replace('rpx', '').replace('upx', '');
        isRpx = true;
    } else if (text.indexOf('px') !== -1) {
        text = text.replace('px', '');
    }
    if (!isNaN(text)) {
        if (isRpx) return Number(rpx2px(text));
        return Number(text);
    }
    return 0;
}
// rpx => px,预留的兼容处理
function rpx2px(rpx) {
    return uni.upx2px(rpx);
}
// åŒæ­¥èŽ·å–ç³»ç»Ÿä¿¡æ¯ï¼Œå…¼å®¹ä¸åŒå¹³å°
function getSystemInfoSync(useCache = false) {
    if (useCache && cachedSystemInfo) {
        return cachedSystemInfo;
    }
    // ç›®å‰åªç”¨åˆ°äº†deviceInfo、appBaseInfo和windowInfo中的信息,因此仅整合这两个信息数据
    const infoTypes = ['DeviceInfo', 'AppBaseInfo', 'WindowInfo'];
    const { deviceInfo, appBaseInfo, windowInfo } = infoTypes.reduce((acc, key) => {
        const method = `get${key}`;
        if (uni[method] && uni.canIUse(method)) {
            acc[key.charAt(0).toLowerCase() + key.slice(1)] = uni[method]();
        }
        return acc;
    }, {});
    // å¦‚æžœdeviceInfo、appBaseInfo和windowInfo都可以从各自专属的api中获取,则整合它们的数据
    if (deviceInfo && appBaseInfo && windowInfo) {
        cachedSystemInfo = { ...deviceInfo, ...appBaseInfo, ...windowInfo };
    } else {
        // ä½¿ç”¨uni.getSystemInfoSync兜底,确保能获取到最终的系统信息
        cachedSystemInfo = uni.getSystemInfoSync();
    }
    return cachedSystemInfo;
}
// èŽ·å–å½“å‰æ—¶é—´
function getTime() {
    return (new Date()).getTime();
}
// èŽ·å–z-paging实例id,随机生成10位数字+字母
function getInstanceId() {
    const s = [];
    const hexDigits = "0123456789abcdef";
    for (let i = 0; i < 10; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    return s.join('') + getTime();
}
// ç­‰å¾…一段时间
function wait(ms) {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}
// æ˜¯å¦æ˜¯promise
function isPromise(func) {
    return Object.prototype.toString.call(func) === '[object Promise]';
}
// æ·»åŠ å•ä½
function addUnit(value, unit) {
    if (Object.prototype.toString.call(value) === '[object String]') {
        let tempValue = value;
        tempValue = tempValue.replace('rpx', '').replace('upx', '').replace('px', '');
        if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1 && value.indexOf('px') !== -1) {
            tempValue = parseFloat(tempValue) * 2;
        }
        value = tempValue;
    }
    return unit === 'rpx' ? value + 'rpx' : (value / 2) + 'px';
}
// æ·±æ‹·è´
function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) return obj;
    let newObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = deepCopy(obj[key]);
        }
    }
    return newObj;
}
// ------------------ ç§æœ‰æ–¹æ³• ------------------------
// å¤„理全局配置
function _handleDefaultConfig() {
    // ç¡®ä¿åªåŠ è½½ä¸€æ¬¡å…¨å±€é…ç½®
    if (configLoaded) return;
    // ä¼˜å…ˆä»Žconfig.js中读取
    if (zLocalConfig && Object.keys(zLocalConfig).length) {
        config = zLocalConfig;
    }
    // å¦‚果在config.js中读取不到,则尝试到uni.$zp读取
    if (!config && uni.$zp) {
        config = uni.$zp.config;
    }
    // å°†config中的短横线写法全部转为驼峰写法,使得读取配置时可以直接通过key去匹配,而非读取每个配置时候再去转,减少不必要的性能开支
    config = config ? Object.keys(config).reduce((result, key) => {
        result[_toCamelCase(key)] = config[key];
        return result;
    }, {}) : null;
    configLoaded = true;
}
// æ—¶é—´æ ¼å¼åŒ–
function _timeFormat(time, textMap) {
    const date = new Date(time);
    const currentDate = new Date();
    // è®¾ç½®time对应的天,去除时分秒,使得可以直接比较日期
    const dateDay = new Date(time).setHours(0, 0, 0, 0);
    // è®¾ç½®å½“前的天,去除时分秒,使得可以直接比较日期
    const currentDateDay = new Date().setHours(0, 0, 0, 0);
    const disTime = dateDay - currentDateDay;
    let dayStr = '';
    const timeStr = _dateTimeFormat(date);
    if (disTime === 0) {
        dayStr = textMap.today;
    } else if (disTime === -86400000) {
        dayStr = textMap.yesterday;
    } else {
        dayStr = _dateDayFormat(date, date.getFullYear() !== currentDate.getFullYear());
    }
    return `${dayStr} ${timeStr}`;
}
// date格式化为年月日
function _dateDayFormat(date, showYear = true) {
    const year = date.getFullYear();
    const month = date.getMonth() + 1;
    const day = date.getDate();
    return showYear ? `${year}-${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}` : `${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}`;
}
// data格式化为时分
function _dateTimeFormat(date) {
    const hour = date.getHours();
    const minute = date.getMinutes();
    return `${_fullZeroToTwo(hour)}:${_fullZeroToTwo(minute)}`;
}
// ä¸æ»¡2位在前面填充0
function _fullZeroToTwo(str) {
    str = str.toString();
    return str.length === 1 ? '0' + str : str;
}
// é©¼å³°è½¬çŸ­æ¨ªçº¿
function _toKebab(value) {
    return value.replace(/([A-Z])/g, "-$1").toLowerCase();
}
// çŸ­æ¨ªçº¿è½¬é©¼å³°
function _toCamelCase(value) {
    return value.replace(/-([a-z])/g, (_, group1) => group1.toUpperCase());
}
export default {
    gc,
    setRefesrherTime,
    getRefesrherFormatTimeByKey,
    getTouch,
    getTouchFromZPaging,
    getParent,
    convertToPx,
    getTime,
    getInstanceId,
    consoleErr,
    delay,
    wait,
    isPromise,
    addUnit,
    deepCopy,
    rpx2px,
    getSystemInfoSync
};
src/components/z-paging/wxs/z-paging-renderjs.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
// [z-paging]使用renderjs在app-vue和h5中对touchmove事件冒泡进行处理
import u from '../js/z-paging-utils'
const data = {
    startY: 0,
    isTouchFromZPaging: false,
    isUsePageScroll: false,
    isReachedTop: true,
    isIosAndH5: false,
    useChatRecordMode: false,
    appLaunched: false
}
export default {
    mounted() {
        if (window) {
            this._handleTouch();
            // #ifdef APP-VUE
            this.$ownerInstance.callMethod('_handlePageLaunch');
            // #endif
        }
    },
    methods: {
        // æŽ¥æ”¶é€»è¾‘层发送的数据(是否是ios+h5)
        renderPropIsIosAndH5Change(newVal) {
            if (newVal === -1) return;
            data.isIosAndH5 = newVal;
        },
        // æ‹¦æˆªå¤„理touch事件
        _handleTouch() {
            if (!window.$zPagingRenderJsInited) {
                window.$zPagingRenderJsInited = true;
                window.addEventListener('touchstart', this._handleTouchstart, { passive: true })
                window.addEventListener('touchmove', this._handleTouchmove, { passive: false })
            }
        },
        // å¤„理touch开始
        _handleTouchstart(e) {
            const touch = u.getTouch(e);
            data.startY = touch.touchY;
            const touchResult = u.getTouchFromZPaging(e.target);
            data.isTouchFromZPaging = touchResult.isFromZp;
            data.isUsePageScroll = touchResult.isPageScroll;
            data.isReachedTop = touchResult.isReachedTop;
            data.useChatRecordMode = touchResult.isUseChatRecordMode;
        },
        // å¤„理touch中
        _handleTouchmove(e) {
            const touch = u.getTouch(e);
            const moveY = touch.touchY - data.startY;
            // å¦‚果是在z-paging内触摸并且(是在顶部位置且是下拉的情况下(或不是聊天记录滚动模式并且在iOS+h5+scroll-view并且是往上拉的情况:避免在此平台中滚动到底部后上拉有个系统灰色遮罩导致列表被短暂锁定的问题))
            // (data.useChatRecordMode ? moveY < 0 : moveY > 0)是为了判断是否是上拉的情况,聊天记录模式列表倒置,因此moveY < 0为上拉
            if (data.isTouchFromZPaging && ((data.isReachedTop && (data.useChatRecordMode ? moveY < 0 : moveY > 0)) || (!data.useChatRecordMode && data.isIosAndH5 && !data.isUsePageScroll && moveY < 0))) {
                if (e.cancelable && !e.defaultPrevented) {
                    // é˜»æ­¢äº‹ä»¶å†’泡,以避免在一些平台中下拉刷新时整个page跟着一起下拉&在iOS+h5+scroll-view中在底部上拉有个系统灰色遮罩导致列表被短暂锁定的问题
                    e.preventDefault();
                }
            }
        },
        // ç§»é™¤touch相关事件监听
        _removeAllEventListener(){
            window.removeEventListener('touchstart');
            window.removeEventListener('touchmove');
        }
    }
};
src/components/z-paging/wxs/z-paging-wxs.wxs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,382 @@
// [z-paging]微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
var currentDis = 0;
var isPCFlag = -1;
var startY = -1;
// ç›‘听js层传过来的数据
function propObserver(newVal, oldVal, ownerIns, ins) {
    var state = ownerIns.getState() || {};
    state.currentIns = ins;
    var dataset = ins.getDataset();
    var loading = dataset.loading == true;
    // å¦‚果是下拉刷新结束,更新transform
    if (newVal && newVal.indexOf('end') != -1) {
        var transition = newVal.split('end')[0];
        _setTransform('translateY(0px)', ins, false, transition);
        state.moveDis = 0;
        state.oldMoveDis = 0;
        currentDis = 0;
    } else if (newVal && newVal.indexOf('begin') != -1) {
        // å¦‚果是下拉刷新开始,更新transform
        var refresherThreshold = ins.getDataset().refresherthreshold;
        _setTransformValue(refresherThreshold, ins, state, false);
    }
}
// touch开始
function touchstart(e, ownerIns) {
    var ins = _getIns(ownerIns);
    var state = {};
    var dataset = {};
    ownerIns.callMethod('_handleListTouchstart');
    if (ins) {
        state = ins.getState();
        dataset = ins.getDataset();
        if (_touchDisabled(e, ins, 0)) return;
    }
    var isTouchEnded = state.isTouchEnded;
    state.oldMoveDis = 0;
    var touch = _getTouch(e);
    var loading = _isTrue(dataset.loading);
    state.startY = touch.touchY;
    startY = state.startY;
    state.lastTouch = touch;
    if (!loading && isTouchEnded) {
        state.isTouchmoving = false;
    }
    state.isTouchEnded = false;
    // é€šçŸ¥js层touch开始
    ownerIns.callMethod('_handleRefresherTouchstart', touch);
}
// touch中
function touchmove(e, ownerIns) {
    var touch = _getTouch(e);
    var ins = _getIns(ownerIns);
    var dataset = ins.getDataset();
    var refresherThreshold = dataset.refresherthreshold;
    var refresherF2Threshold = dataset.refresherf2threshold;
    var refresherF2Enabled = _isTrue(dataset.refresherf2enabled);
    var isIos = _isTrue(dataset.isios);
    var state = ins.getState();
    var watchTouchDirectionChange = _isTrue(dataset.watchtouchdirectionchange);
    var moveDisObj = {};
    var moveDis = 0;
    var prevent = false;
    // å¦‚果需要监听touch方向的改变
    if (watchTouchDirectionChange) {
        moveDisObj = _getMoveDis(e, ins);
        moveDis = moveDisObj.currentDis;
        prevent = moveDisObj.isDown;
        var direction = prevent ? 'top' : 'bottom';
        // ç¡®ä¿åªåœ¨touch方向改变时通知一次js层,而不是touchmove中持续通知
        if (prevent == state.oldTouchDirection && prevent != state.oldEmitedTouchDirection) {
            ownerIns.callMethod('_handleTouchDirectionChange', { direction: direction });
            state.oldEmitedTouchDirection = prevent;
        }
        state.oldTouchDirection = prevent;
    }
    // åˆ¤æ–­æ˜¯å¦å…è®¸ä¸‹æ‹‰åˆ·æ–°
    if (_touchDisabled(e, ins, 1)) {
        _handlePullingDown(state, ownerIns, false);
        return true;
    }
    // åˆ¤æ–­ä¸‹æ‹‰åˆ·æ–°çš„角度是否在要求范围内
    if (!_getAngleIsInRange(e, touch, state, dataset)) {
        _handlePullingDown(state, ownerIns, false);
        return true;
    }
    moveDisObj = _getMoveDis(e, ins);
    moveDis = moveDisObj.currentDis;
    prevent = moveDisObj.isDown;
    if (moveDis < 0) {
        // moveDis小于0,将transform重置为0
        _setTransformValue(0, ins, state, false);
        _handlePullingDown(state, ownerIns, false);
        return true;
    }
    if (prevent && !state.disabledBounce) {
        // å¦‚果是用户下拉并且需要触发下拉刷新,需要通知js层将列表禁止滚动,防止在下拉刷新过程中列表也可以滚动导致的下拉刷新偏移过大的问题(在下拉刷新过程中仅通知一次)
        ownerIns.callMethod('_handleScrollViewBounce', { bounce: false });
        state.disabledBounce = true;
        _handlePullingDown(state, ownerIns, prevent);
        return !prevent;
    }
    // æ›´æ–°transform
    _setTransformValue(moveDis, ins, state, false);
    var oldRefresherStatus = state.refresherStatus;
    var oldIsTouchmoving = _isTrue(dataset.oldistouchmoving);
    var hasTouchmove = _isTrue(dataset.hastouchmove);
    var isTouchmoving = state.isTouchmoving;
    state.refresherStatus = moveDis >= refresherThreshold ? (refresherF2Enabled && moveDis > refresherF2Threshold ? 'goF2' : 'releaseToRefresh') : 'default';
    if (!isTouchmoving) {
        state.isTouchmoving = true;
        isTouchmoving = true;
    }
    if (state.isTouchEnded) {
        state.isTouchEnded = false;
    }
    // å¦‚果需要实时监听下拉位置偏移,则需要实时通知js层,此操作会使wxs层与js层频繁通信从而导致在一些性能较差设备中下拉刷新卡顿
    if (hasTouchmove) {
        ownerIns.callMethod('_handleWxsPullingDown', { moveDis: moveDis, diffDis: moveDisObj.diffDis });
    }
    // åœ¨ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ”¹å˜æ—¶é€šçŸ¥js层
    if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving != isTouchmoving) {
        ownerIns.callMethod('_handleRefresherTouchmove', moveDis, touch);
    }
    _handlePullingDown(state, ownerIns, prevent);
    return !prevent;
}
// touch结束
function touchend(e, ownerIns) {
    var touch = _getTouch(e);
    var ins = _getIns(ownerIns);
    var dataset = ins.getDataset();
    var state = ins.getState();
    if (state.disabledBounce) {
        // é€šçŸ¥js允许列表滚动
        ownerIns.callMethod('_handleScrollViewBounce', { bounce: true });
        state.disabledBounce = false;
    }
    if (_touchDisabled(e, ins, 2)) return;
    state.reachMaxAngle = true;
    state.hitReachMaxAngleCount = 0;
    state.fixedIsTopHitCount = 0;
    if (!state.isTouchmoving) return;
    var oldRefresherStatus = state.refresherStatus;
    var oldMoveDis = state.moveDis;
    var refresherThreshold = ins.getDataset().refresherthreshold;
    var moveDis = _getMoveDis(e, ins).currentDis;
    if (!(moveDis >= refresherThreshold && oldRefresherStatus === 'releaseToRefresh')) {
        state.isTouchmoving = false;
    }
    // é€šçŸ¥js层touch结束
    ownerIns.callMethod('_handleRefresherTouchend', moveDis);
    state.isTouchEnded = true;
    if (oldMoveDis < refresherThreshold) return;
    var animate = false;
    if (moveDis >= refresherThreshold) {
        moveDis = refresherThreshold;
        animate = true;
    }
    _setTransformValue(moveDis, ins, state, animate);
}
// #ifdef H5
// åˆ¤æ–­æ˜¯å¦æ˜¯pc平台
function isPC() {
    if (!navigator) return false;
    if (isPCFlag != -1) return isPCFlag;
    var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
    isPCFlag = agents.every(function(item) { return navigator.userAgent.indexOf(item) < 0 });
    return isPCFlag;
}
var movable = false;
// åœ¨pc平台监听mousedown、mousemove、mouseup等相关事件并转为对应touch事件处理,使得在pc平台也支持通过鼠标进行下拉刷新
function mousedown(e, ins) {
    if (!isPC()) return;
    touchstart(e, ins);
    movable = true;
}
function mousemove(e, ins) {
    if (!isPC() || !movable) return;
    touchmove(e, ins);
}
function mouseup(e, ins) {
    if (!isPC()) return;
    touchend(e, ins);
    movable = false;
}
function mouseleave(e, ins) {
    if (!isPC()) return;
    movable = false;
}
// #endif
// ä¿®æ”¹è§†å›¾å±‚transform
function _setTransformValue(value, ins, state, animate) {
    value = value || 0;
    if (state.moveDis == value) return;
    state.moveDis = value;
    _setTransform('translateY(' + value + 'px)', ins, animate, '');
}
// è®¾ç½®è§†å›¾å±‚transform,直接在视图层操作下拉刷新,使得js层不需要频繁和视图层通信,从而大大提升下拉刷新性能
function _setTransform(transform, ins, animate, transition) {
    var dataset = ins.getDataset();
    if (_isTrue(dataset.refreshernotransform)) return;
    transform = transform == 'translateY(0px)' ? 'none' : transform;
    ins.requestAnimationFrame(function() {
        var stl = { 'transform': transform };
        if (animate) {
            stl['transition'] = 'transform .1s linear';
        }
        if (transition.length) {
            stl['transition'] = transition;
        }
        ins.setStyle(stl);
    })
}
// è¿›ä¸€æ­¥å¤„理下拉刷新的偏移数据
function _getMoveDis(e, ins) {
    var state = ins.getState();
    var refresherThreshold = parseFloat(ins.getDataset().refresherthreshold);
    var refresherOutRate = parseFloat(ins.getDataset().refresheroutrate);
    var refresherPullRate = parseFloat(ins.getDataset().refresherpullrate);
    var touch = _getTouch(e);
    var currentStartY = !state.startY || state.startY == 'NaN' ? startY : state.startY;
    var moveDis = touch.touchY - currentStartY;
    var oldMoveDis = state.oldMoveDis || 0;
    state.oldMoveDis = moveDis;
    // èŽ·å–å½“å‰ä¸‹æ‹‰åˆ·æ–°ä½ç½®ä¸Žä¸Šæ¬¡çš„åç§»é‡
    var diffDis = moveDis - oldMoveDis;
    if (diffDis > 0) {
        // å¯¹åç§»é‡è¿›è¡Œè¿›ä¸€æ­¥å¤„理,通过refresherPullRate等配置进行约束
        diffDis = diffDis * refresherPullRate;
        if (currentDis > refresherThreshold) {
            diffDis = diffDis * (1 - refresherOutRate);
        }
    }
    // æŽ§åˆ¶diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
    diffDis = diffDis > 100 ? diffDis / 100 : (diffDis > 20 ? diffDis / 2.2 : diffDis);
    currentDis += diffDis;
    currentDis = Math.max(0, currentDis);
    return {
        currentDis: currentDis,
        diffDis: diffDis,
        isDown: diffDis > 0
    };
}
// èŽ·å–ç»è¿‡ç»Ÿä¸€æ ¼å¼åŒ…è£…çš„å½“å‰touch对象
function _getTouch(e) {
    var touch = e;
    if (e.touches && e.touches.length) {
        touch = e.touches[0];
    } else if (e.changedTouches && e.changedTouches.length) {
        touch = e.changedTouches[0];
    } else if (e.datail && e.datail != {}) {
        touch = e.datail;
    }
    return {
        touchX: touch.clientX,
        touchY: touch.clientY
    };
}
// èŽ·å–å½“å‰currentIns
function _getIns(ownerIns) {
    var ins = ownerIns.getState().currentIns;
    if (!ins) {
        ownerIns.callMethod('_handlePropUpdate');
    }
    return ins;
}
// åˆ¤æ–­å½“前状态是否允许下拉刷新
function _touchDisabled(e, ins, processTag) {
    var dataset = ins.getDataset();
    var state = ins.getState();
    var loading = _isTrue(dataset.loading);
    var useChatRecordMode = _isTrue(dataset.usechatrecordmode);
    var refresherEnabled = _isTrue(dataset.refresherenabled);
    var useCustomRefresher = _isTrue(dataset.usecustomrefresher);
    var usePageScroll = _isTrue(dataset.usepagescroll);
    var pageScrollTop = parseFloat(dataset.pagescrolltop);
    var scrollTop = parseFloat(dataset.scrolltop);
    var finalScrollTop = usePageScroll ? pageScrollTop : scrollTop;
    var fixedIsTop = false;
    // æ˜¯å¦è¦å¤„理滚动到顶部scrollTop不为0时候的容错,为解决在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
    var handleFaultTolerantMove = false;
    if (handleFaultTolerantMove && finalScrollTop == (state.startScrollTop || 0) && finalScrollTop <= 105) {
        fixedIsTop = true;
    }
    var fixedIsTopHitCount = state.fixedIsTopHitCount || 0;
    if (fixedIsTop) {
        fixedIsTopHitCount ++;
        if (fixedIsTopHitCount <= 2) {
            fixedIsTop = false;
        }
        state.fixedIsTopHitCount = fixedIsTopHitCount;
    } else {
        state.fixedIsTopHitCount = 0;
    }
    if (handleFaultTolerantMove && processTag === 0) {
        state.startScrollTop = finalScrollTop || 0;
    }
    if (handleFaultTolerantMove && processTag === 2) {
        fixedIsTop = true;
    }
    return loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher ||
    ((usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) ||
    ((!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
}
// åˆ¤æ–­ä¸‹æ‹‰åˆ·æ–°çš„角度是否在要求范围内
function _getAngleIsInRange(e, touch, state, dataset) {
    var maxAngle = dataset.refreshermaxangle;
    var refresherAecc = _isTrue(dataset.refresheraecc);
    var lastTouch = state.lastTouch;
    var reachMaxAngle = state.reachMaxAngle;
    var moveDis = state.oldMoveDis;
    if (!lastTouch) return true;
    if (maxAngle >= 0 && maxAngle <= 90 && lastTouch) {
        // è€ƒè™‘下拉刷新手势由水平移动转为垂直方向移动的情况,此时不应当只判断垂直方向角度是否符合要求,应当直接禁止以避免在swiper中使用下拉刷新时,横向切换swiper途中手未离开屏幕还可以下拉刷新的问题
        if ((!moveDis || moveDis < 1) && !refresherAecc && reachMaxAngle != null && !reachMaxAngle) return false;
        var x = Math.abs(touch.touchX - lastTouch.touchX);
        var y = Math.abs(touch.touchY - lastTouch.touchY);
        var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        if ((x || y) && x > 1) {
            // èŽ·å–ä¸‹æ‹‰åˆ·æ–°å‰åŽä¸¤æ¬¡ä½ç§»çš„è§’åº¦
            var angle = Math.asin(y / z) / Math.PI * 180;
            if (angle < maxAngle) {
                // å¦‚果角度小于配置要求,则return,同时通过hitReachMaxAngleCount控制角度判断的灵敏程度以最大程度兼容各种使用场景
                var hitReachMaxAngleCount = state.hitReachMaxAngleCount || 0;
                state.hitReachMaxAngleCount = ++hitReachMaxAngleCount;
                if (state.hitReachMaxAngleCount > 2) {
                    state.lastTouch = touch;
                    state.reachMaxAngle = false;
                }
                return false;
            }
        }
    }
    state.lastTouch = touch;
    return true;
}
// è¿›ä¸€æ­¥å¤„理是否在下拉刷新并通知js层
function _handlePullingDown(state, ins, onPullingDown) {
    var oldOnPullingDown = state.onPullingDown || false;
    if (oldOnPullingDown != onPullingDown) {
        ins.callMethod('_handleWxsPullingDownStatusChange', onPullingDown);
    }
    state.onPullingDown = onPullingDown;
}
// åˆ¤æ–­js层传过来的值是否为true
function _isTrue(value) {
    value = (typeof(value) === 'string' ? JSON.parse(value) : value) || false;
    return value == true || value == 'true';
}
module.exports = {
    touchstart: touchstart,
    touchmove: touchmove,
    touchend: touchend,
    mousedown: mousedown,
    mousemove: mousemove,
    mouseup: mouseup,
    mouseleave: mouseleave,
    propObserver: propObserver
}
src/components/z-paging/z-paging.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,538 @@
 <!--                        _
  ____     _ __   __ _  __ _(_)_ __   __ _
 |_  /____| '_ \ / _` |/ _` | | '_ \ / _` |
  / /_____| |_) | (_| | (_| | | | | | (_| |
 /___|    | .__/ \__,_|\__, |_|_| |_|\__, |
          |_|          |___/         |___/
v2.8.6 (2025-03-17)
@author ZXLee <admin@zxlee.cn>
-->
<!-- æ–‡æ¡£åœ°å€ï¼šhttps://z-paging.zxlee.cn -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
<!-- åé¦ˆQQ群:343409055 -->
<template name="z-paging">
    <!-- #ifndef APP-NVUE -->
    <view :class="{'z-paging-content':true,'z-paging-content-full':!usePageScroll,'z-paging-content-fixed':!usePageScroll&&fixed,'z-paging-content-page':usePageScroll,'z-paging-reached-top':renderPropScrollTop<1,'z-paging-use-chat-record-mode':useChatRecordMode}" :style="[finalPagingStyle]">
        <!-- #ifndef APP-PLUS -->
        <view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
        <!-- #endif -->
        <!-- äºŒæ¥¼view -->
        <view v-if="showF2 && showRefresherF2" @touchmove.stop.prevent class="zp-f2-content" :style="[{'transform': f2Transform, 'transition': `transform .2s linear`, 'height': superContentHeight + 'px', 'z-index': f2ZIndex}]">
            <slot name="f2"/>
        </view>
        <!-- é¡¶éƒ¨å›ºå®šçš„slot -->
        <slot v-if="!usePageScroll&&zSlots.top" name="top" />
        <view class="zp-page-top" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
            <slot name="top" />
        </view>
        <view :class="{'zp-view-super':true,'zp-scroll-view-super':!usePageScroll}" :style="[finalScrollViewStyle]">
            <view v-if="zSlots.left" :class="{'zp-page-left':true,'zp-absoulte':finalIsOldWebView}">
                <slot name="left" />
            </view>
            <view :class="{'zp-scroll-view-container':true,'zp-absoulte':finalIsOldWebView}" :style="[scrollViewContainerStyle]">
                <scroll-view
                    ref="zp-scroll-view" :class="{'zp-scroll-view':true,'zp-scroll-view-absolute':!usePageScroll,'zp-scroll-view-hide-scrollbar':!showScrollbar}" :style="[chatRecordRotateStyle]"
                    :scroll-top="scrollTop" :scroll-left="scrollLeft" :scroll-x="scrollX"
                    :scroll-y="finalScrollable" :enable-back-to-top="finalEnableBackToTop"
                    :show-scrollbar="showScrollbar" :scroll-with-animation="finalScrollWithAnimation"
                    :scroll-into-view="scrollIntoView" :lower-threshold="finalLowerThreshold" :upper-threshold="5"
                    :refresher-enabled="finalRefresherEnabled&&!useCustomRefresher" :refresher-threshold="finalRefresherThreshold"
                    :refresher-default-style="finalRefresherDefaultStyle" :refresher-background="refresherBackground"
                    :refresher-triggered="finalRefresherTriggered" @scroll="_scroll" @scrolltolower="_onScrollToLower"
                    @scrolltoupper="_onScrollToUpper" @refresherrestore="_onRestore" @refresherrefresh="_onRefresh(true)"
                    >
                    <view class="zp-paging-touch-view"
                    <!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5 -->
                    @touchstart="_refresherTouchstart" @touchmove="_refresherTouchmove" @touchend="_refresherTouchend" @touchcancel="_refresherTouchend"
                    <!-- #endif -->
                    <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                    @touchstart="pagingWxs.touchstart" @touchmove="pagingWxs.touchmove" @touchend="pagingWxs.touchend" @touchcancel="pagingWxs.touchend"
                    @mousedown="pagingWxs.mousedown" @mousemove="pagingWxs.mousemove" @mouseup="pagingWxs.mouseup" @mouseleave="pagingWxs.mouseleave"
                    <!-- #endif -->
                    >
                        <view v-if="finalRefresherFixedBacHeight>0" class="zp-fixed-bac-view" :style="[{'background': refresherFixedBackground,'height': `${finalRefresherFixedBacHeight}px`}]"></view>
                        <view class="zp-paging-main" :style="[scrollViewInStyle,{'transform': finalRefresherTransform,'transition': refresherTransition}]"
                        <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                        :change:prop="pagingWxs.propObserver" :prop="wxsPropType"
                        :data-refresherThreshold="finalRefresherThreshold" :data-refresherF2Enabled="refresherF2Enabled" :data-refresherF2Threshold="finalRefresherF2Threshold" :data-isIos="isIos"
                        :data-loading="loading||isRefresherInComplete" :data-useChatRecordMode="useChatRecordMode"
                        :data-refresherEnabled="refresherEnabled" :data-useCustomRefresher="useCustomRefresher" :data-pageScrollTop="wxsPageScrollTop"
                        :data-scrollTop="wxsScrollTop" :data-refresherMaxAngle="refresherMaxAngle" :data-refresherNoTransform="refresherNoTransform"
                        :data-refresherAecc="refresherAngleEnableChangeContinued" :data-usePageScroll="usePageScroll" :data-watchTouchDirectionChange="watchTouchDirectionChange"
                        :data-oldIsTouchmoving="isTouchmoving" :data-refresherOutRate="finalRefresherOutRate" :data-refresherPullRate="finalRefresherPullRate" :data-hasTouchmove="hasTouchmove"
                        <!-- #endif -->
                        <!-- #ifdef APP-VUE || H5 -->
                        :change:renderPropIsIosAndH5="pagingRenderjs.renderPropIsIosAndH5Change" :renderPropIsIosAndH5="isIosAndH5"
                        <!-- #endif -->
                        >
                            <view v-if="showRefresher" class="zp-custom-refresher-view" :style="[{'margin-top': `-${finalRefresherThreshold+refresherThresholdUpdateTag}px`,'background': refresherBackground,'opacity': isTouchmoving ? 1 : 0}]">
                                <view class="zp-custom-refresher-container" :style="[{'height': `${finalRefresherThreshold}px`,'background': refresherBackground}]">
                                    <view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
                                    <!-- ä¸‹æ‹‰åˆ·æ–°view -->
                                    <view class="zp-custom-refresher-slot-view">
                                        <slot v-if="!(zSlots.refresherComplete&&refresherStatus===R.Complete)&&!(zSlots.refresherF2&&refresherStatus===R.GoF2)" :refresherStatus="refresherStatus" name="refresher" />
                                    </view>
                                    <slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
                                    <slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
                                    <z-paging-refresh ref="refresh" v-else-if="!showCustomRefresher" class="zp-custom-refresher-refresh" :style="[{'height': `${finalRefresherThreshold - finalRefresherThresholdPlaceholder}px`}]" :status="refresherStatus"
                                        :defaultThemeStyle="finalRefresherThemeStyle" :defaultText="finalRefresherDefaultText" :isIos="isIos"
                                        :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
                                        :defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
                                        :showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
                                        :imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
                                </view>
                            </view>
                            <view class="zp-paging-container" :style="[{justifyContent:useChatRecordMode?'flex-end':'flex-start'}]">
                                <!-- å…¨å±Loading -->
                                <slot v-if="showLoading&&zSlots.loading&&!loadingFullFixed" name="loading" />
                                <!-- ä¸»ä½“内容 -->
                                <view class="zp-paging-container-content" :style="[finalPlaceholderTopHeightStyle,finalPagingContentStyle]">
                                    <!-- #ifdef VUE3 -->
                                    <!-- è™šæ‹Ÿåˆ—表顶部占位view -->
                                    <view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderTopHeight+'px'}]"/>
                                    <!-- #endif -->
                                    <slot />
                                    <!-- å†…置列表&虚拟列表 -->
                                    <template v-if="finalUseInnerList">
                                        <slot name="header"/>
                                        <view class="zp-list-container" :style="[innerListStyle]">
                                            <template v-if="finalUseVirtualList">
                                                <view class="zp-list-cell" :style="[innerCellStyle]" :id="`${fianlVirtualCellIdPrefix}-${item[virtualCellIndexKey]}`" v-for="(item,index) in virtualList" :key="item['zp_unique_index']" @click="_innerCellClick(item,virtualTopRangeIndex+index)">
                                                    <view v-if="useCompatibilityMode">使用兼容模式请在组件源码z-paging.vue第103行中注释这一行,并打开下面一行注释</view>
                                                    <!-- <zp-public-virtual-cell v-if="useCompatibilityMode" :extraData="extraData" :item="item" :index="virtualTopRangeIndex+index" /> -->
                                                    <slot v-else name="cell" :item="item" :index="virtualTopRangeIndex+index"/>
                                                </view>
                                            </template>
                                            <template v-else>
                                                <view class="zp-list-cell" v-for="(item,index) in realTotalData" :key="index" @click="_innerCellClick(item,index)">
                                                    <slot name="cell" :item="item" :index="index"/>
                                                </view>
                                            </template>
                                        </view>
                                        <slot name="footer"/>
                                    </template>
                                    <!-- èŠå¤©è®°å½•模式加载更多loading -->
                                    <template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&(realTotalData.length||(showChatLoadingWhenReload&&showLoading))&&!isFirstPageAndNoMore">
                                        <view :style="[chatRecordRotateStyle]">
                                            <slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
                                            <template v-else>
                                                <slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
                                                <z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
                                            </template>
                                        </view>
                                    </template>
                                    <!-- è™šæ‹Ÿåˆ—表底部占位view -->
                                    <view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderBottomHeight+'px'}]"/>
                                    <!-- ä¸Šæ‹‰åŠ è½½æ›´å¤šview -->
                                    <!-- #ifndef MP-ALIPAY -->
                                    <slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
                                    <slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
                                    <slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
                                    <slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
                                    <z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
                                    <!-- #endif -->
                                    <!-- #ifdef MP-ALIPAY -->
                                    <slot v-if="loadingStatus===M.Default&&zSlots.loadingMoreDefault&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreDefault" />
                                    <slot v-else-if="loadingStatus===M.Loading&&zSlots.loadingMoreLoading&&showLoadingMore&&loadingMoreEnabled" name="loadingMoreLoading" />
                                    <slot v-else-if="loadingStatus===M.NoMore&&zSlots.loadingMoreNoMore&&showLoadingMore&&showLoadingMoreNoMoreView&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreNoMore" />
                                    <slot v-else-if="loadingStatus===M.Fail&&zSlots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreFail" />
                                    <z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===M.NoMore&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :zConfig="zLoadMoreConfig" />
                                    <!-- #endif -->
                                    <view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
                                </view>
                                <!-- ç©ºæ•°æ®å›¾ -->
                                <view v-if="showEmpty" :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}" :style="[emptyViewSuperStyle,chatRecordRotateStyle]">
                                    <slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed"/>
                                    <z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload"
                                    :emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle"
                                    :emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
                                    @reload="_emptyViewReload" @viewClick="_emptyViewClick" />
                                </view>
                            </view>
                        </view>
                    </view>
                </scroll-view>
            </view>
            <view v-if="zSlots.right" :class="{'zp-page-right':true,'zp-absoulte zp-right':finalIsOldWebView}">
                <slot name="right" />
            </view>
        </view>
        <!-- åº•部固定的slot -->
        <view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
            <slot v-if="!usePageScroll&&zSlots.bottom" name="bottom" />
            <view class="zp-page-bottom" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.bottom" :style="[{'bottom': `${windowBottom}px`}]">
                <slot name="bottom" />
            </view>
            <!-- èŠå¤©è®°å½•模式底部占位 -->
            <template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
                <view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
                <view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
            </template>
        </view>
        <!-- ç‚¹å‡»è¿”回顶部view -->
        <view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
            <slot v-if="zSlots.backToTop" name="backToTop" />
            <image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
        </view>
        <!-- å…¨å±Loading(铺满z-paging并固定) -->
        <view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
            <slot name="loading" />
        </view>
    </view>
    <!-- #endif -->
    <!-- #ifdef APP-NVUE -->
    <component ref="z-paging-content" :is="finalNvueSuperListIs" :style="[finalPagingStyle]" :class="{'z-paging-content-fixed':fixed&&!usePageScroll}" :scrollable="false">
        <!-- äºŒæ¥¼view -->
        <view v-if="showF2 && showRefresherF2" ref="zp-n-f2" class="zp-f2-content" @touchmove.stop.prevent :style="[{'height': superContentHeight + 'px', 'width': nRefresherWidth + 'px', 'opacity': nF2Opacity}]">
            <slot name="f2"/>
        </view>
        <!-- é¡¶éƒ¨å›ºå®šçš„slot -->
        <view ref="zp-page-top" v-if="zSlots.top" :class="{'zp-page-top':usePageScroll}" :style="[usePageScroll?{'top':`${windowTop}px`,'z-index':topZIndex}:{}]">
            <slot name="top" />
        </view>
        <!-- èŠå¤©è®°å½•模式加载更多loading(loading时候显示) -->
        <view v-if="useChatRecordMode&&loadingStatus!==M.NoMore&&showChatLoadingWhenReload&&showLoading">
            <slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
            <z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
        </view>
        <component :is="finalNvueSuperListIs" class="zp-n-list-container" :scrollable="false">
            <view v-if="zSlots.left" class="zp-page-left">
                <slot name="left" />
            </view>
            <component :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},chatRecordRotateStyle]" :alwaysScrollableVertical="true"
                :fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar" :loadmoreoffset="finalLowerThreshold" :enable-back-to-top="enableBackToTop"
                :scrollable="finalScrollable" :bounce="nvueBounce" :column-count="nWaterfallColumnCount" :column-width="nWaterfallColumnWidth"
                :column-gap="nWaterfallColumnGap" :left-gap="nWaterfallLeftGap" :right-gap="nWaterfallRightGap" :pagingEnabled="nvuePagingEnabled" :offset-accuracy="offsetAccuracy"
                @loadmore="_nOnLoadmore" @scroll="_nOnScroll" @scrollend="_nOnScrollend">
                <refresh v-if="(zSlots.top?cacheTopHeight!==-1:true)&&finalNvueRefresherEnabled" class="zp-n-refresh" :style="[nvueRefresherStyle]" :display="nRefresherLoading?'show':'hide'" @refresh="_nOnRrefresh" @pullingdown="_nOnPullingdown">
                    <view ref="zp-n-refresh-container" class="zp-n-refresh-container" :style="[{background:refresherBackground,width:nRefresherWidth}]" id="zp-n-refresh-container">
                        <view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
                        <!-- ä¸‹æ‹‰åˆ·æ–°view -->
                        <slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
                        <slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
                        <slot v-else-if="(nScopedSlots?nScopedSlots:zSlots).refresher" :refresherStatus="refresherStatus" name="refresher" />
                        <z-paging-refresh ref="refresh" v-else :status="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
                            :defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
                            :defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
                            :showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
                            :imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
                    </view>
                </refresh>
                <component :is="nViewIs" v-if="isIos&&!useChatRecordMode?oldScrollTop>10:true" ref="zp-n-list-top-tag" class="zp-n-list-top-tag" style="margin-top: -1rpx;" :style="[{height:finalNvueRefresherEnabled?'0px':'1px'}]"></component>
                <component :is="nViewIs" v-if="nShowRefresherReveal" ref="zp-n-list-refresher-reveal" :style="[{transform:`translateY(-${nShowRefresherRevealHeight}px)`},{background:refresherBackground}]">
                    <view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
                    <!-- ä¸‹æ‹‰åˆ·æ–°view -->
                    <slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
                    <slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
                    <slot v-else-if="(nScopedSlots?nScopedSlots:$slots).refresher" :refresherStatus="R.Loading" name="refresher" />
                    <z-paging-refresh ref="refresh" v-else :status="R.Loading" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
                        :defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
                        :defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
                        :showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
                        :imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
                </component>
                <!-- å†…置列表 -->
                <template v-if="finalUseInnerList">
                    <component :is="nViewIs">
                        <slot name="header"/>
                    </component>
                    <component :is="nViewIs" class="zp-list-cell" v-for="(item,index) in realTotalData" :key="finalCellKeyName.length?item[finalCellKeyName]:index">
                        <slot name="cell" :item="item" :index="index"/>
                    </component>
                    <component :is="nViewIs">
                        <slot name="footer"/>
                    </component>
                </template>
                <template v-else>
                    <slot />
                </template>
                <!-- å…¨å±Loading -->
                <component :is="nViewIs" v-if="showLoading&&zSlots.loading&&!loadingFullFixed" :class="{'z-paging-content-fixed':usePageScroll}" style="flex:1" :style="[chatRecordRotateStyle]">
                    <slot name="loading" />
                </component>
                <!-- ä¸Šæ‹‰åŠ è½½æ›´å¤šview -->
                <component :is="nViewIs" v-if="!refresherOnly&&loadingMoreEnabled&&!showEmpty">
                    <!-- èŠå¤©è®°å½•模式加载更多loading(滚动到顶部加载更多或无更多数据时显示) -->
                    <template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&realTotalData.length&&isChatRecordModeAndInversion">
                        <view :style="[chatRecordRotateStyle]">
                            <slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
                            <template v-else>
                                <slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
                                <z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
                            </template>
                        </view>
                    </template>
                    <view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:loadingMoreFixedHeight}:{}">
                        <slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
                        <slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
                        <slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
                        <slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
                        <z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
                        <view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
                    </view>
                </component>
                <!-- ç©ºæ•°æ®å›¾ -->
                <component :is="nViewIs" v-if="showEmpty" :class="{'z-paging-content-fixed':usePageScroll}" :style="[{flex:emptyViewCenter?1:0},emptyViewSuperStyle,chatRecordRotateStyle]">
                    <view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}">
                        <slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed" />
                        <z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload"
                        :emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle"
                        :emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
                        @reload="_emptyViewReload" @viewClick="_emptyViewClick" />
                    </view>
                </component>
                <component :is="nViewIs" v-if="!hideNvueBottomTag" ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag"></component>
            </component>
            <view v-if="zSlots.right" class="zp-page-right">
                <slot name="right" />
            </view>
        </component>
        <!-- åº•部固定的slot -->
        <view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
            <slot name="bottom" />
            <!-- èŠå¤©è®°å½•模式底部占位 -->
            <template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
                <view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
                <view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
            </template>
        </view>
        <!-- ç‚¹å‡»è¿”回顶部view -->
        <view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
            <slot v-if="zSlots.backToTop" name="backToTop" />
            <image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
        </view>
        <!-- å…¨å±Loading(铺满z-paging并固定) -->
        <view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
            <slot name="loading" />
        </view>
    </component>
    <!-- #endif -->
</template>
<script module="pagingRenderjs" lang="renderjs">
    import pagingRenderjs from './wxs/z-paging-renderjs.js';
    /**
     * z-paging åˆ†é¡µç»„ä»¶
     * @description z-paging åˆ†é¡µç»„件,高性能,全平台兼容。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、全自动分页、无闪动聊天分页、本地分页等,也支持作为基本布局容器使用
     * @tutorial https://z-paging.zxlee.cn
     * @property {Array} value çˆ¶ç»„ä»¶v-model所绑定的list的值,默认为[]
     * @property {Number|String} defaultPageNo è‡ªå®šä¹‰åˆå§‹çš„pageNo,默认为1
     * @property {Number|String} defaultPageSize è‡ªå®šä¹‰pageSize(每页显示多少条),默认为10
     * @property {Boolean} fixed z-paging是否使用fixed布局,默认为true
     * @property {Boolean} safeAreaInsetBottom æ˜¯å¦å¼€å¯åº•部安全区域适配,默认为false
     * @property {Boolean} useSafeAreaPlaceholder å¼€å¯åº•部安全区域适配后,是否使用placeholder形式实现,默认为false
     * @property {Boolean} usePageScroll ä½¿ç”¨é¡µé¢æ»šåŠ¨ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Boolean} autoFullHeight ä½¿ç”¨é¡µé¢æ»šåŠ¨æ—¶ï¼Œæ˜¯å¦åœ¨ä¸æ»¡å±æ—¶è‡ªåŠ¨å¡«å……æ»¡å±å¹•ï¼Œé»˜è®¤ä¸ºtrue
     * @property {String} defaultThemeStyle loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认为black
     * @property {Object} pagingStyle è®¾ç½®z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
     * @property {String} height z-paging的高度,优先级低于pagingStyle中设置的height,传字符串,如100px、100rpx、100%
     * @property {String} width z-paging的宽度,优先级低于pagingStyle中设置的width,传字符串,如100px、100rpx、100%
     * @property {String} maxWidth z-paging的最大宽度,优先级低于pagingStyle中设置的max-width,默认为空
     * @property {String} bgColor z-paging的背景色(为css中的background,因此也可以设置渐变,背景图片等),优先级低于pagingStyle中设置的background-color
     * @property {Boolean} watchTouchDirectionChange æ˜¯å¦ç›‘听列表触摸方向改变,默认为false
     * @property {Number|String} delay è°ƒç”¨complete后延迟处理的时间,单位为毫秒,优先级高于min-delay,默认为0
     * @property {Number|String} minDelay è§¦å‘@query后最小延迟处理的时间,单位为毫秒,优先级低于delay,默认为0
     * @property {Boolean} callNetworkReject è¯·æ±‚失败是否触发reject,默认为true
     * @property {String} unit z-paging中默认布局的单位,默认为rpx
     * @property {Boolean} concat è‡ªåŠ¨æ‹¼æŽ¥complete中传过来的数组,默认为true
     * @property {Number|String|Object} dataKey ä¸ºä¿è¯æ•°æ®ä¸€è‡´ï¼Œè®¾ç½®å½“前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
     * @property {String} autowireListName ã€æžç®€å†™æ³•】自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
     * @property {String} autowireQueryName ã€æžç®€å†™æ³•】自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
     * @property {Function} fetch ã€æžç®€å†™æ³•】获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
     * @property {Object} fetchParams fetch的附加参数,fetch配置后有效
     * @property {Boolean} auto [z-paging]mounted后是否自动调用reload方法(mounted后自动调用接口),默认为true
     * @property {Boolean} autoScrollToTopWhenReload reload时自动滚动到顶部,默认为true
     * @property {Boolean} autoCleanListWhenReload reload时立即自动清空原list,默认为true
     * @property {Boolean} showRefresherWhenReload åˆ—表刷新时自动显示下拉刷新view,默认为false
     * @property {Boolean} showLoadingMoreWhenReload åˆ—表刷新时自动显示加载更多view,且为加载中状态,默认为false
     * @property {Boolean} createdReload ç»„ä»¶created时立即触发reload,默认为false
     * @property {Boolean} refresherEnabled æ˜¯å¦å¼€å¯ä¸‹æ‹‰åˆ·æ–°ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Number|String} refresherThreshold è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é˜ˆå€¼ï¼Œé»˜è®¤å•位为px,默认为80rpx
     * @property {Boolean} useRefresherStatusBarPlaceholder æ˜¯å¦å¼€å¯ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ å ä½ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Boolean} refresherOnly æ˜¯å¦åªä½¿ç”¨ä¸‹æ‹‰åˆ·æ–°ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Boolean} useCustomRefresher æ˜¯å¦ä½¿ç”¨è‡ªå®šä¹‰çš„下拉刷新,默认为true
     * @property {Boolean} reloadWhenRefresh ç”¨æˆ·ä¸‹æ‹‰åˆ·æ–°æ—¶æ˜¯å¦è§¦å‘reload方法,默认为true
     * @property {String} refresherThemeStyle ä¸‹æ‹‰åˆ·æ–°çš„主题样式,支持black,white,默认为black
     * @property {Object} refresherImgStyle è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å·¦ä¾§å›¾æ ‡çš„æ ·å¼
     * @property {Object} refresherTitleStyle è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å³ä¾§çŠ¶æ€æè¿°æ–‡å­—çš„æ ·å¼
     * @property {Object} refresherUpdateTimeStyle è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸­å³ä¾§æœ€åŽæ›´æ–°æ—¶é—´æ–‡å­—的样式
     * @property {Boolean} watchRefresherTouchmove æ˜¯å¦å®žæ—¶ç›‘听下拉刷新中进度,并通过@refresherTouchmove传递给父组件,默认为false
     * @property {Boolean} showRefresherUpdateTime æ˜¯å¦æ˜¾ç¤ºæœ€åŽæ›´æ–°æ—¶é—´ï¼Œé»˜è®¤ä¸ºfalse
     * @property {String|Object} refresherDefaultText è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹çš„æ–‡å­—
     * @property {String|Object} refresherPullingText è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°æ¾æ‰‹ç«‹å³åˆ·æ–°çŠ¶æ€ä¸‹çš„æ–‡å­—
     * @property {String|Object} refresherRefreshingText è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹çš„æ–‡å­—
     * @property {String|Object} refresherCompleteText è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束状态下的文字
     * @property {String} refresherDefaultImg è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹çš„å›¾ç‰‡
     * @property {String} refresherPullingImg è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°æ¾æ‰‹ç«‹å³åˆ·æ–°çŠ¶æ€ä¸‹çš„å›¾ç‰‡
     * @property {String} refresherRefreshingImg è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹çš„å›¾ç‰‡
     * @property {String} refresherCompleteImg è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束状态下的图片
     * @property {Boolean} refresherRefreshingAnimated è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€ä¸‹æ˜¯å¦å±•ç¤ºæ—‹è½¬åŠ¨ç”»ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Boolean} refresherEndBounceEnabled æ˜¯å¦å¼€å¯è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ç»“束回弹动画效果,默认为true
     * @property {String} refresherDefaultStyle è®¾ç½®ç³»ç»Ÿä¸‹æ‹‰åˆ·æ–°é»˜è®¤æ ·å¼ï¼Œæ”¯æŒè®¾ç½®black,white,none,默认为black
     * @property {String} refresherBackground è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åŒºåŸŸèƒŒæ™¯é¢œè‰²ï¼Œé»˜è®¤ä¸º#FFFFFF00
     * @property {String} refresherFixedBackground è®¾ç½®å›ºå®šçš„自定义下拉刷新区域背景颜色,默认为#FFFFFF00
     * @property {Number|String} refresherFixedBacHeight è®¾ç½®å›ºå®šçš„自定义下拉刷新区域高度,默认为0
     * @property {Number|String} refresherDefaultDuration è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°é»˜è®¤çŠ¶æ€ä¸‹å›žå¼¹åŠ¨ç”»æ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤ä¸º100
     * @property {Number|String} refresherCompleteDelay è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束以后延迟收回的时间,单位为毫秒,默认为0
     * @property {Number|String} refresherCompleteDuration è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束收回动画时间,单位为毫秒,默认为300
     * @property {Boolean} refresherVibrate ä¸‹æ‹‰åˆ·æ–°æ—¶ä¸‹æ‹‰åˆ°â€œæ¾æ‰‹ç«‹å³åˆ·æ–°â€çŠ¶æ€æ—¶æ˜¯å¦ä½¿æ‰‹æœºçŸ­æŒ¯åŠ¨ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Boolean} refresherRefreshingScrollable è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°åˆ·æ–°ä¸­çŠ¶æ€æ˜¯å¦å…è®¸åˆ—è¡¨æ»šåŠ¨ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Boolean} refresherCompleteScrollable è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ç»“束状态下是否允许列表滚动,默认为false
     * @property {Number} refresherOutRate è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰è¶…出阈值后继续下拉位移衰减的比例,默认为0.65
     * @property {Boolean} refresherF2Enabled æ˜¯å¦å¼€å¯ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼åŠŸèƒ½ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Number|String} refresherF2Threshold ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼é˜ˆå€¼ï¼Œé»˜è®¤ä¸º200rpx
     * @property {Number|String} refresherF2Duration ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼åŠ¨ç”»æ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤ä¸º200
     * @property {Boolean} showRefresherF2 ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼çŠ¶æ€æ¾æ‰‹åŽæ˜¯å¦å¼¹å‡ºäºŒæ¥¼ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Number} refresherPullRate è®¾ç½®è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰æ—¶å®žé™…下拉位移与用户下拉距离的比值,默认为0.75
     * @property {Number|String} refresherFps è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰å¸§çŽ‡ï¼Œé»˜è®¤ä¸º40
     * @property {Number|String} refresherMaxAngle è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°å…è®¸è§¦å‘的最大下拉角度,默认为40度
     * @property {Boolean} refresherAngleEnableChangeContinued è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°çš„角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为false
     * @property {Boolean} refresherNoTransform ä¸‹æ‹‰åˆ·æ–°æ—¶æ˜¯å¦ç¦æ­¢ä¸‹æ‹‰åˆ·æ–°view跟随用户触摸竖直移动,默认为false
     * @property {Boolean} loadingMoreEnabled æ˜¯å¦å¯ç”¨åŠ è½½æ›´å¤šæ•°æ®(含滑动到底部加载更多数据和点击加载更多数据),默认为true
     * @property {Number|String} lowerThreshold è·åº•部/右边多远时,触发scrolltolower事件,默认单位为px,默认为100rpx
     * @property {Boolean} toBottomLoadingMoreEnabled æ˜¯å¦å¯ç”¨æ»‘动到底部加载更多数据,默认为true
     * @property {String} loadingMoreThemeStyle åº•部加载更多的主题样式,支持black,white,默认为black
     * @property {Object} loadingMoreCustomStyle è‡ªå®šä¹‰åº•部加载更多样式
     * @property {Object} loadingMoreTitleCustomStyle è‡ªå®šä¹‰åº•部加载更多文字样式
     * @property {Object} loadingMoreLoadingIconCustomStyle è‡ªå®šä¹‰åº•部加载更多加载中动画样式
     * @property {String} loadingMoreLoadingIconType è‡ªå®šä¹‰åº•部加载更多加载中动画图标类型,可选flower或circle,默认为flower
     * @property {String} loadingMoreLoadingIconCustomImage è‡ªå®šä¹‰åº•部加载更多加载中动画图标图片
     * @property {Boolean} loadingMoreLoadingAnimated åº•部加载更多加载中view是否展示旋转动画,默认为true
     * @property {String|Object} loadingMoreDefaultText æ»‘动到底部"默认"文字
     * @property {String|Object} loadingMoreLoadingText æ»‘动到底部"加载中"文字
     * @property {String|Object} loadingMoreNoMoreText æ»‘动到底部"没有更多"文字
     * @property {String|Object} loadingMoreFailText æ»‘动到底部"加载失败"文字
     * @property {Boolean} hideNoMoreInside å½“没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为false
     * @property {Number} hideNoMoreByLimit å½“没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0
     * @property {Boolean} insideMore å½“分页未满一屏时,是否自动加载更多,默认为false
     * @property {Boolean} loadingMoreDefaultAsLoading æ»‘动到底部状态为默认状态时,以加载中的状态展示,默认为false
     * @property {Boolean} showLoadingMoreNoMoreView æ˜¯å¦æ˜¾ç¤ºæ²¡æœ‰æ›´å¤šæ•°æ®çš„view,默认为true
     * @property {Boolean} showDefaultLoadingMoreText æ˜¯å¦æ˜¾ç¤ºé»˜è®¤çš„加载更多text,默认为true
     * @property {Boolean} showLoadingMoreNoMoreLine æ˜¯å¦æ˜¾ç¤ºæ²¡æœ‰æ›´å¤šæ•°æ®çš„分割线,默认为true
     * @property {Object} loadingMoreNoMoreLineCustomStyle è‡ªå®šä¹‰åº•部没有更多数据的分割线样式
     * @property {Boolean} hideEmptyView æ˜¯å¦å¼ºåˆ¶éšè—ç©ºæ•°æ®å›¾ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Boolean} emptyViewFixed ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦é“ºæ»¡z-paging,默认为false
     * @property {Boolean} emptyViewCenter ç©ºæ•°æ®å›¾ç‰‡æ˜¯å¦åž‚直居中,默认为true
     * @property {String|Object} emptyViewText ç©ºæ•°æ®å›¾æè¿°æ–‡å­—
     * @property {String} emptyViewImg ç©ºæ•°æ®å›¾å›¾ç‰‡
     * @property {String} emptyViewErrorImg ç©ºæ•°æ®å›¾â€œåŠ è½½å¤±è´¥â€å›¾ç‰‡
     * @property {String|Object} emptyViewReloadText ç©ºæ•°æ®å›¾ç‚¹å‡»é‡æ–°åŠ è½½æ–‡å­—
     * @property {String|Object} emptyViewErrorText ç©ºæ•°æ®å›¾â€œåŠ è½½å¤±è´¥â€æè¿°æ–‡å­—
     * @property {Object} emptyViewSuperStyle ç©ºæ•°æ®å›¾çˆ¶view样式
     * @property {Object} emptyViewStyle ç©ºæ•°æ®å›¾æ ·å¼
     * @property {Object} emptyViewImgStyle ç©ºæ•°æ®å›¾img样式
     * @property {Object} emptyViewTitleStyle ç©ºæ•°æ®å›¾æè¿°æ–‡å­—样式
     * @property {Object} emptyViewReloadStyle ç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®æ ·å¼
     * @property {Boolean} showEmptyViewReload æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®(无数据时),默认为false
     * @property {Boolean} showEmptyViewReloadWhenError åŠ è½½å¤±è´¥æ—¶æ˜¯å¦æ˜¾ç¤ºç©ºæ•°æ®å›¾é‡æ–°åŠ è½½æŒ‰é’®ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Boolean} autoHideEmptyViewWhenLoading åŠ è½½ä¸­æ—¶æ˜¯å¦è‡ªåŠ¨éšè—ç©ºæ•°æ®å›¾ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Boolean} autoHideEmptyViewWhenPull ç”¨æˆ·ä¸‹æ‹‰åˆ—表触发下拉刷新加载中时是否自动隐藏空数据图,默认为true
     * @property {Boolean} autoHideLoadingAfterFirstLoaded ç¬¬ä¸€æ¬¡åŠ è½½åŽè‡ªåŠ¨éšè—loading slot,默认为true
     * @property {Boolean} loadingFullFixed loading slot的父view是否铺满屏幕并固定,默认为false
     * @property {Boolean} autoShowSystemLoading æ˜¯å¦è‡ªåŠ¨æ˜¾ç¤ºç³»ç»ŸLoading:即uni.showLoading,默认为false
     * @property {String|Object} systemLoadingText æ˜¾ç¤ºç³»ç»ŸLoading时显示的文字
     * @property {Boolean} systemLoadingMask æ˜¾ç¤ºç³»ç»ŸLoading时是否显示透明蒙层,防止触摸穿透,默认为true
     * @property {Boolean} autoShowBackToTop è‡ªåŠ¨æ˜¾ç¤ºç‚¹å‡»è¿”å›žé¡¶éƒ¨æŒ‰é’®ï¼Œé»˜è®¤ä¸ºfalse
     * @property {Number|String} backToTopThreshold ç‚¹å‡»è¿”回顶部按钮显示/隐藏的阈值(滚动距离),默认单位为px,默认为400rpx
     * @property {String} backToTopImg ç‚¹å‡»è¿”回顶部按钮的自定义图片地址
     * @property {Boolean} backToTopWithAnimate ç‚¹å‡»è¿”回顶部按钮返回到顶部时是否展示过渡动画,默认为true
     * @property {Number|String} backToTopBottom ç‚¹å‡»è¿”回顶部按钮与底部的距离,默认单位为px,默认为160rpx
     * @property {Object} backToTopStyle ç‚¹å‡»è¿”回顶部按钮的自定义样式
     * @property {Boolean} useVirtualList æ˜¯å¦ä½¿ç”¨è™šæ‹Ÿåˆ—表,默认为false
     * @property {Boolean} useCompatibilityMode åœ¨ä½¿ç”¨è™šæ‹Ÿåˆ—表时,是否使用兼容模式,默认为false
     * @property {Object} extraData ä½¿ç”¨å…¼å®¹æ¨¡å¼æ—¶ä¼ é€’的附加数据
     * @property {String} cellHeightMode è™šæ‹Ÿåˆ—表cell高度模式,默认为fixed
     * @property {Number|String} preloadPage é¢„加载的列表可视范围(列表高度)页数,默认为12
     * @property {Number|String} fixedCellHeight å›ºå®šçš„cell高度,`cell-height-mode=fixed`才有效,默认为空
     * @property {Number|String} virtualListCol è™šæ‹Ÿåˆ—表列数,默认为1
     * @property {Number|String} virtualScrollFps è™šæ‹Ÿåˆ—表scroll取样帧率,默认为80
     * @property {String} virtualCellIdPrefix è™šæ‹Ÿåˆ—表cell id的前缀
     * @property {Boolean} useInnerList æ˜¯å¦åœ¨z-paging内部循环渲染列表(使用内置列表),默认为false
     * @property {Boolean} forceCloseInnerList å¼ºåˆ¶å…³é—­inner-list,默认为false
     * @property {Boolean} virtualInSwiperSlot è™šæ‹Ÿåˆ—表是否使用swiper-item包裹,默认为false
     * @property {String} cellKeyName å†…置列表cell的key名称(仅nvue有效)
     * @property {Object} innerListStyle innerList样式
     * @property {Object} innerCellStyle innerCell样式
     * @property {Number|String} localPagingLoadingTime æœ¬åœ°åˆ†é¡µæ—¶ä¸Šæ‹‰åŠ è½½æ›´å¤šå»¶è¿Ÿæ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’ï¼Œé»˜è®¤ä¸º200
     * @property {Boolean} useChatRecordMode ä½¿ç”¨èŠå¤©è®°å½•模式,默认为false
     * @property {Boolean} autoHideKeyboardWhenChat ä½¿ç”¨èŠå¤©è®°å½•模式时是否自动隐藏键盘,默认为true
     * @property {Boolean} autoAdjustPositionWhenChat ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时是否自动调整slot="bottom"高度,默认为true
     * @property {Boolean} autoToBottomWhenChat ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时是否自动滚动到底部,默认为false
     * @property {String} chatAdjustPositionOffset ä½¿ç”¨èŠå¤©è®°å½•模式中键盘弹出时占位高度偏移距离,默认为0px
     * @property {Boolean} showChatLoadingWhenReload ä½¿ç”¨èŠå¤©è®°å½•模式中`reload`时是否显示`chatLoading`,默认为false
     * @property {String} bottomBgColor `bottom`的背景色,默认透明
     * @property {Boolean} chatLoadingMoreDefaultAsLoading åœ¨èŠå¤©è®°å½•模式中滑动到顶部状态为默认状态时,是否以加载中的状态展示,默认为true
     * @property {Boolean} showScrollbar æŽ§åˆ¶æ˜¯å¦å‡ºçŽ°æ»šåŠ¨æ¡ï¼Œé»˜è®¤ä¸ºtrue
     * @property {Boolean} scrollable æ˜¯å¦å¯ä»¥æ»šåŠ¨ï¼Œä½¿ç”¨å†…ç½®scroll-view和nvue时有效,默认为true
     * @property {Boolean} scrollX æ˜¯å¦å…è®¸æ¨ªå‘滚动,默认为false
     * @property {Boolean} scrollToTopBounceEnabled iOS设备上滚动到顶部时是否允许回弹效果,默认为false
     * @property {Boolean} scrollToBottomBounceEnabled iOS设备上滚动到底部时是否允许回弹效果,默认为true
     * @property {Boolean} scrollWithAnimation åœ¨è®¾ç½®æ»šåŠ¨æ¡ä½ç½®æ—¶ä½¿ç”¨åŠ¨ç”»è¿‡æ¸¡ï¼Œé»˜è®¤ä¸ºfalse
     * @property {String} scrollIntoView å€¼åº”为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
     * @property {Boolean} enableBackToTop iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,默认为true
     * @property {String} nvueListIs nvue中修改列表类型,默认为list
     * @property {Object} nvueWaterfallConfig waterfall配置,仅在nvue中且nvueListIs=waterfall时有效
     * @property {Boolean} nvueBounce nvue控制是否回弹效果,iOS不支持动态修改,默认为true
     * @property {Boolean} nvueFastScroll nvue中通过代码滚动到顶部/底部时,是否加快动画效果,默认为false
     * @property {String} nvueListId nvue中list的id
     * @property {Boolean} hideNvueBottomTag æ˜¯å¦éšè—nvue列表底部的tagView,默认为false
     * @property {Boolean} nvuePagingEnabled è®¾ç½®nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
     * @property {Number} offsetAccuracy nvue中控制onscroll事件触发的频率,默认为空
     * @property {Boolean} useCache æ˜¯å¦ä½¿ç”¨ç¼“存,默认为false
     * @property {String} cacheKey ä½¿ç”¨ç¼“存时缓存的key
     * @property {String} cacheMode ç¼“存模式,默认为default
     * @property {Number} topZIndex slot="top"的view的z-index,默认为99
     * @property {Number} superContentZIndex z-paging内容容器父view的z-index,默认为1
     * @property {Number} contentZIndex z-paging内容容器部分的z-index,默认为1
     * @property {Number} emptyViewZIndex ç©ºæ•°æ®view的z-index,默认为9
     * @property {Boolean} autoHeight z-paging是否自动高度,默认为false
     * @property {Number|String} autoHeightAddition z-paging自动高度时的附加高度,默认为0px
     * @event {Function} input çˆ¶ç»„ä»¶v-model所绑定的list的值改变时触发此事件
     * @event {Function} query ä¸‹æ‹‰åˆ·æ–°æˆ–滚动到底部时会自动触发此方法。z-paging加载时也会触发(若要禁止,请设置:auto="false")。pageNo和pageSize会自动计算好,直接传给服务器即可。
     * @event {Function} listChange åˆ†é¡µæ¸²æŸ“的数组改变时触发
     * @event {Function} refresherStatusChange è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ”¹å˜
     * @event {Function} refresherTouchstart è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰å¼€å§‹
     * @event {Function} refresherTouchmove è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰æ‹–动中
     * @event {Function} refresherTouchend è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°ä¸‹æ‹‰ç»“束
     * @event {Function} refresherF2Change ä¸‹æ‹‰è¿›å…¥äºŒæ¥¼çŠ¶æ€æ”¹å˜
     * @event {Function} refresh è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°è¢«è§¦å‘
     * @event {Function} restore è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°è¢«å¤ä½
     * @event {Function} loadingStatusChange è‡ªå®šä¹‰ä¸‹æ‹‰åˆ·æ–°çŠ¶æ€æ”¹å˜
     * @event {Function} emptyViewReload ç‚¹å‡»äº†ç©ºæ•°æ®å›¾ä¸­çš„重新加载按钮
     * @event {Function} emptyViewClick ç‚¹å‡»äº†ç©ºæ•°æ®å›¾view
     * @event {Function} isLoadFailedChange z-paging请求失败状态改变
     * @event {Function} backToTopClick ç‚¹å‡»äº†è¿”回顶部按钮
     * @event {Function} virtualListChange è™šæ‹Ÿåˆ—表当前渲染的数组改变时触发
     * @event {Function} innerCellClick ä½¿ç”¨è™šæ‹Ÿåˆ—表或内置列表时点击了cell
     * @event {Function} virtualPlaceholderTopHeight è™šæ‹Ÿåˆ—表顶部占位高度改变
     * @event {Function} hidedKeyboard åœ¨èŠå¤©è®°å½•模式下,触摸列表隐藏了键盘
     * @event {Function} keyboardHeightChange é”®ç›˜é«˜åº¦æ”¹å˜
     * @event {Function} scroll z-paging列表滚动时触发
     * @event {Function} scrollTopChange scrollTop改变时触发
     * @event {Function} scrolltolower z-paging内置的scroll-view/list-view/waterfall滚动底部时触发
     * @event {Function} scrolltoupper z-paging内置的scroll-view/list-view/waterfall滚动顶部时触发
     * @event {Function} scrollend z-paging内置的list滚动结束时触发
     * @event {Function} contentHeightChanged z-paging中内容高度改变时触发
     * @event {Function} touchDirectionChange ç›‘听列表触摸方向改变
     * @example <z-paging ref="paging" v-model="dataList" @query="queryList"></z-paging>
     */
    export default {
        name:"z-paging",
        // #ifdef APP-VUE || H5
        mixins: [pagingRenderjs],
        // #endif
    }
</script>
<script src="./js/z-paging-main.js" />
<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
<script src="./wxs/z-paging-wxs.wxs" module="pagingWxs" lang="wxs"></script>
<!-- #endif -->
<style scoped>
    @import "./css/z-paging-main.css";
    @import "./css/z-paging-static.css";
</style>
src/hooks/useZebraScan.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
export function useZebraScan() {
  // pda的特色设置中需要关闭-禁止将扫描按键的键值传递给应用的开关
  const mainActivity = ref(); // è¿è¡Œæ—¶çš„主要活动activity
  const intentFilter = ref(); // å®žä¾‹åŒ–的意图过滤器
  const broadcastReceiver = ref(); // å¹¿æ’­æŽ¥æ”¶å™¨
  const repeatFlag = ref<boolean>(false);
  const init = (onReceiveCallback: any) => {
    console.log("初始化zebra扫描");
    /* #ifdef APP-PLUS */
    // èŽ·å–activity
    mainActivity.value = plus.android.runtimeMainActivity();
    // å¯¼å…¥ç±»
    const IntentFilter = plus.android.importClass("android.content.IntentFilter");
    //实例化一个意图过滤器
    intentFilter.value = new IntentFilter();
    // addAction添加动作, com.android.server.scannerservice.broadcast为设备配置的广播名称
    intentFilter.value.addAction("com.dwexample.ACTION");
    // BroadcastReceiver广播接收器接口:implements å®žçŽ°æŽ¥å£  onReceive实现接口的方法
    broadcastReceiver.value = plus.android.implements(
      "io.dcloud.feature.internal.reflect.BroadcastReceiver",
      {
        onReceive: (context: any, intent: any) => {
          plus.android.importClass(intent);
          // æ‰«æè®¾ç½®çš„开发者选项--手持机pda的广播键值scannerdata
          const code = intent.getStringExtra("com.motorolasolutions.emdk.datawedge.data_string");
          console.log("pda原始扫描数据:", code);
          //防重复
          if (repeatFlag.value) return;
          repeatFlag.value = true;
          setTimeout(() => {
            repeatFlag.value = false;
          }, 150);
          // åˆ°è¿™é‡Œæ‰«ææˆåŠŸ,获取结果,可以调用自己的业务逻辑
          onReceiveCallback(code);
        },
      }
    );
    console.log("注册广播成功");
    /* #endif */
  };
  const start = () => {
    console.log("开始扫描");
    mainActivity.value.registerReceiver(broadcastReceiver, intentFilter);
  };
  const stop = () => {
    console.log("停止扫描");
    mainActivity.value.unregisterReceiver(broadcastReceiver);
  };
  //是否开启激光红外线扫描,true开启,false关闭
  const triggerScan = () => {
    console.log("触发扫描");
    // èŽ·å–Android意图类
    let Intent = plus.android.importClass("android.content.Intent");
    // å®žä¾‹åŒ–意图
    let intent = new Intent();
    // å®šä¹‰æ„å›¾ï¼Œç”±åŽ‚å•†æä¾›(此处设置为东大的: å¼€å§‹æ‰«æå¹¿æ’­com.scan.onStartScan,对应的停止扫描广播为com.scan.onEndScan)
    intent.setAction("com.symbol.datawedge.api.ACTION");
    intent.putExtra("com.symbol.datawedge.api.SOFT_SCAN_TRIGGER", "START_SCANNING");
    // å¹¿æ’­è¿™ä¸ªæ„å›¾
    mainActivity.value.sendBroadcast(intent);
  };
  return { init, start, stop, triggerScan };
}
src/pages.json
@@ -3,7 +3,7 @@
    "autoscan": true,
    "custom": {
      "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
      "^cu-(.*)": "@/components/cu-$1/index.vue"
      "^cu-(.*)": "@/components/cu-$1/index.vue",
    }
  },
src/pages/production/detail/twistDetail.vue
@@ -9,12 +9,12 @@
          url="/pages/production/twist/report/index"
          text="报工"
        />
        <wd-grid-item
        <!-- <wd-grid-item
          icon="chart"
          text="自检"
          link-type="navigateTo"
          url="/pages/production/twist/selfInspect/index"
        />
        /> -->
        <wd-grid-item
          icon="tips"
          link-type="navigateTo"
src/pages/production/detail/wireDetail.vue
@@ -9,12 +9,12 @@
          url="/pages/production/wire/report/wire"
          text="报工"
        />
        <wd-grid-item
        <!-- <wd-grid-item
          icon="chart"
          text="自检"
          link-type="navigateTo"
          url="/pages/production/wire/selfInspect/index"
        />
        /> -->
        <wd-grid-item
          icon="tips"
          link-type="navigateTo"
src/pages/production/twist/receive/monofil.vue
@@ -2,48 +2,64 @@
  <view class="page">
    <CardTitle title="单丝领用" :hideAction="false">
      <template #action>
        <wd-button type="icon" icon="scan" color="#0D867F" @click="scanCode"></wd-button>
        <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
      </template>
    </CardTitle>
    <view class="list_box">
      <MonofilCard v-for="(item, index) in 4" :key="index" />
      <MonofilCard v-for="(item, index) in cardList" :key="index" />
    </view>
    <scan />
    <Scan ref="scanRef" />
  </view>
</template>
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import MonofilCard from "../components/MonofilCard.vue";
import scan from "@/components/scan/index.vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import Scan from "@/components/scan/index.vue";
// import { useZebraScan } from "@/hooks/useZebraScan";
// const { init, start, stop, triggerScan } = useZebraScan();
const scanRef = ref();
const cardList = ref<any[]>([]);
const BroadcastScanningToObtainData = (res: any) => {
  console.log("获取次数", res.code);
  let barcode = res.code;
  console.log("打印数据", barcode);
const getScanCode = (code: any) => {
  // let parseData = code.trim();
  console.log("自定义扫描的结果回调函数:", code);
  cardList.value.push({});
};
const scanCode = () => {
  uni.scanCode({
    onlyFromCamera: true,
    success: (res) => {
      console.log("条码类型:" + res.scanType);
      console.log("条码内容:" + res.result);
      cardList.value.push(res.result);
    },
  });
const openScan = () => {
  // uni.scanCode({
  //   onlyFromCamera: true,
  //   success: (res) => {
  //     console.log("条码类型:" + res.scanType);
  //     console.log("条码内容:" + res.result);
  //     cardList.value.push(res.result);
  //   },
  // });
  // triggerScan();
  scanRef.value.triggerScan();
};
onLoad(() => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  uni.$on("scan", BroadcastScanningToObtainData);
  uni.$on("scan", getScanCode);
  // init(getScanCode);
});
onUnload(() => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  uni.$off("scan", BroadcastScanningToObtainData);
  uni.$off("scan", getScanCode);
  // stop();
});
onShow(() => {
  // start();
});
onHide(() => {
  // stop();
});
</script>
src/pages/production/twist/receive/steelCore/index.vue
@@ -57,7 +57,7 @@
  {
    label: "厂家",
    value: "江苏省南通市芯导数字厂",
    span: 14,
    span: 16,
  },
]);
src/pages/production/twist/report/index.vue
@@ -1,7 +1,9 @@
<template>
  <view class="page pt-2">
    <CardTitle title="报工信息" :hideAction="true" @action="addReport" />
    <view class="list">
    <z-paging ref="paging" refresher-only class="list">
      <template #top>
        <CardTitle title="报工信息" :hideAction="true" :full="false" @action="addReport" />
      </template>
      <wd-card v-for="(item, index) in 6" type="rectangle" custom-class="round">
        <template #title>
          <view class="flex justify-between">
@@ -14,9 +16,12 @@
          </view>
        </template>
        <ProductionCard :data="cardAttr" />
        <template #footer>
          <wd-button size="small" plain @click="toCheck">自检</wd-button>
        </template>
      </wd-card>
      <wd-loadmore custom-class="loadmore" state="loading" />
    </view>
    </z-paging>
    <wd-popup v-model="dialog.visible" position="bottom" custom-class="yl-popup">
      <view class="action px-3">
@@ -97,6 +102,12 @@
  toast.show("取消");
  dialog.visible = false;
};
const toCheck = () => {
  uni.navigateTo({
    url: "/pages/production/twist/selfInspect/index",
  });
};
</script>
<style lang="scss" scoped>
@@ -104,9 +115,7 @@
  background: #f3f9f8;
  .list {
    height: calc(100vh - 120px);
    margin: 12px;
    overflow: scroll;
    :deep() {
      .round {
src/pages/production/wire/backman/index.vue
@@ -28,7 +28,7 @@
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import ProductionCard from "../components/ProductionCard.vue";
import ProductionCard from "../../components/ProductionCard.vue";
import { useToast } from "wot-design-uni";
import BackmanForm from "./form.vue";
@@ -66,7 +66,7 @@
const toEdit = () => {
  uni.navigateTo({
    url: "/pages/production/backman/edit",
    url: "/pages/production/wire/backman/edit",
  });
};
src/pages/production/wire/receive/index.vue
@@ -1,7 +1,7 @@
<template>
  <view class="page pt-2">
    <CardTitle title="拉丝领用" :hideAction="true" @action="addReport" />
    <view class="list">
    <z-paging ref="paging" refresher-only class="list">
      <wd-card type="rectangle" custom-class="round">
        <template #title>
          <view class="flex justify-between">
@@ -14,7 +14,7 @@
        </template>
        <ProductionCard :data="cardAttr" color="#0D867F" />
      </wd-card>
    </view>
    </z-paging>
    <wd-popup v-model="dialog.visible" position="bottom" custom-class="yl-popup">
      <view class="action px-3">
        <wd-button type="text" @click="cancel">取消</wd-button>
@@ -31,6 +31,7 @@
import ProductionCard from "../../components/ProductionCard.vue";
import { useToast } from "wot-design-uni";
import ReceiveForm from "./form.vue";
import zPaging from "@/components/z-paging/z-paging.vue";
const toast = useToast();
const dialog = reactive({
@@ -58,7 +59,7 @@
const toEdit = () => {
  uni.navigateTo({
    url: "/pages/production/receive/edit",
    url: "/pages/production/wire/receive/edit",
  });
};
@@ -83,7 +84,6 @@
  .list {
    height: calc(100vh - 120px);
    margin: 12px;
    overflow: scroll;
    :deep() {
      .round {
src/pages/production/wire/report/wire.vue
@@ -1,7 +1,9 @@
<template>
  <view class="page pt-2">
    <CardTitle title="报工信息" :hideAction="true" @action="addReport" />
    <view class="list">
    <z-paging ref="paging" refresher-only class="list">
      <template #top>
        <CardTitle title="报工信息" :hideAction="true" :full="false" @action="addReport" />
      </template>
      <wd-card v-for="(item, index) in 6" type="rectangle" custom-class="round">
        <template #title>
          <view class="flex justify-between">
@@ -14,9 +16,12 @@
          </view>
        </template>
        <ProductionCard :data="cardAttr" />
        <template #footer>
          <wd-button size="small" plain @click="toCheck">自检</wd-button>
        </template>
      </wd-card>
      <wd-loadmore custom-class="loadmore" state="loading" />
    </view>
    </z-paging>
    <wd-popup v-model="dialog.visible" position="bottom" custom-class="yl-popup">
      <view class="action px-3">
@@ -33,6 +38,7 @@
import WireForm from "./wireForm.vue";
import { useToast } from "wot-design-uni";
import ProductionCard from "../../components/ProductionCard.vue";
import zPaging from "@/components/z-paging/z-paging.vue";
const toast = useToast();
const dialog = reactive({
@@ -76,7 +82,7 @@
const toEdit = () => {
  uni.navigateTo({
    url: "/pages/production/report/wireEdit",
    url: "/pages/production/wire/report/wireEdit",
  });
};
@@ -93,6 +99,12 @@
  toast.show("取消");
  dialog.visible = false;
};
const toCheck = () => {
  uni.navigateTo({
    url: "/pages/production/wire/selfInspect/index",
  });
};
</script>
<style lang="scss" scoped>
@@ -100,9 +112,8 @@
  background: #f3f9f8;
  .list {
    height: calc(100vh - 120px);
    margin: 12px;
    overflow: scroll;
    background: #f3f9f8;
    :deep() {
      .round {
src/pages/production/wire/selfInspect/index.vue
@@ -28,7 +28,7 @@
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import ProductionCard from "../components/ProductionCard.vue";
import ProductionCard from "../../components/ProductionCard.vue";
import { useToast } from "wot-design-uni";
import SelfInspectForm from "./form.vue";
@@ -61,7 +61,7 @@
const toEdit = () => {
  uni.navigateTo({
    url: "/pages/production/selfInspect/edit",
    url: "/pages/production/wire/selfInspect/edit",
  });
};
const submit = () => {