Browse Source

优惠券已经更改部分细节问题

zaijin 2 years ago
parent
commit
454f0c0da9
57 changed files with 8834 additions and 2242 deletions
  1. 3 0
      App.vue
  2. 12 8
      common/apiurl.js
  3. 5 1
      common/http.api.js
  4. 226 0
      components/swiper-list-item/swiper-list-item.vue
  5. 342 328
      pages.json
  6. 35 0
      pages/center/coupon/couponRules/couponRules.vue
  7. 26 0
      pages/center/coupon/myCoupon/myCoupon.scss
  8. 146 0
      pages/center/coupon/myCoupon/myCoupon.vue
  9. 3 3
      pages/center/index.vue
  10. 27 0
      static/img/have-overdued-icon.svg
  11. 27 0
      static/img/have-used-icon.svg
  12. 952 0
      static/quill/quill.bubble.scss
  13. 399 0
      static/quill/quill.core.scss
  14. 945 0
      static/quill/quill.snow.scss
  15. 4 38
      uni_modules/z-paging/changelog.md
  16. 34 0
      uni_modules/z-paging/components/z-paging-cell/z-paging-cell.vue
  17. 36 44
      uni_modules/z-paging/components/z-paging-empty-view/z-paging-empty-view.vue
  18. 68 11
      uni_modules/z-paging/components/z-paging-swiper-item/z-paging-swiper-item.vue
  19. 135 28
      uni_modules/z-paging/components/z-paging-swiper/z-paging-swiper.vue
  20. 68 85
      uni_modules/z-paging/components/z-paging/components/z-paging-load-more.vue
  21. 89 166
      uni_modules/z-paging/components/z-paging/components/z-paging-refresh.vue
  22. 3 0
      uni_modules/z-paging/components/z-paging/config/index.js
  23. 80 10
      uni_modules/z-paging/components/z-paging/css/z-paging-main.css
  24. 5 8
      uni_modules/z-paging/components/z-paging/css/z-paging-static.css
  25. 22 0
      uni_modules/z-paging/components/z-paging/i18n/en.json
  26. 8 0
      uni_modules/z-paging/components/z-paging/i18n/index.js
  27. 22 0
      uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json
  28. 22 0
      uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json
  29. 100 0
      uni_modules/z-paging/components/z-paging/js/modules/back-to-top.js
  30. 740 0
      uni_modules/z-paging/components/z-paging/js/modules/data-handle.js
  31. 148 0
      uni_modules/z-paging/components/z-paging/js/modules/empty.js
  32. 96 0
      uni_modules/z-paging/components/z-paging/js/modules/i18n.js
  33. 318 0
      uni_modules/z-paging/components/z-paging/js/modules/load-more.js
  34. 93 0
      uni_modules/z-paging/components/z-paging/js/modules/loading.js
  35. 249 0
      uni_modules/z-paging/components/z-paging/js/modules/nvue.js
  36. 662 0
      uni_modules/z-paging/components/z-paging/js/modules/refresher.js
  37. 445 0
      uni_modules/z-paging/components/z-paging/js/modules/scroller.js
  38. 400 0
      uni_modules/z-paging/components/z-paging/js/modules/virtual-list.js
  39. 7 18
      uni_modules/z-paging/components/z-paging/js/z-paging-config.js
  40. 12 0
      uni_modules/z-paging/components/z-paging/js/z-paging-constant.js
  41. 44 0
      uni_modules/z-paging/components/z-paging/js/z-paging-enum.js
  42. 54 0
      uni_modules/z-paging/components/z-paging/js/z-paging-interceptor.js
  43. 126 791
      uni_modules/z-paging/components/z-paging/js/z-paging-main.js
  44. 7 19
      uni_modules/z-paging/components/z-paging/js/z-paging-mixin.js
  45. 1 11
      uni_modules/z-paging/components/z-paging/js/z-paging-static.js
  46. 106 78
      uni_modules/z-paging/components/z-paging/js/z-paging-utils.js
  47. 29 30
      uni_modules/z-paging/components/z-paging/wxs/z-paging-renderjs.js
  48. 167 152
      uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs
  49. 268 191
      uni_modules/z-paging/components/z-paging/z-paging.vue
  50. 21 15
      uni_modules/z-paging/package.json
  51. 13 206
      uni_modules/z-paging/readme.md
  52. 2 0
      uni_modules/z-tabs/changelog.md
  53. 4 0
      uni_modules/z-tabs/components/z-tabs/config/index.js
  54. 721 0
      uni_modules/z-tabs/components/z-tabs/z-tabs.vue
  55. 82 0
      uni_modules/z-tabs/package.json
  56. 174 0
      uni_modules/z-tabs/readme.md
  57. 1 1
      uview-ui/components/u-read-more/u-read-more.vue

+ 3 - 0
App.vue

@@ -43,6 +43,9 @@ export default {
 </script>
 
 <style lang="scss">
+@import '/static/quill/quill.bubble.scss';
+@import '/static/quill/quill.snow.scss';
+@import '/static/quill/quill.core.scss';
 @import './static/css/iconfont.css';
 @import 'uview-ui/index.scss';
 /*每个页面公共css */

+ 12 - 8
common/apiurl.js

@@ -45,10 +45,10 @@ const apiurl = {
   nearRoadslUrl: '/client/roadinfo/nearRoads',
   // 获取路段收费规则
   roadsChargeRuleUrl: '/client/roadinfo/feeRule/',
-	// 获取附近的停车场
-	nearParkingLotUrl: '/client/parkingInfo/pageList',
-	// 获取停车场收费规则
-	parkingLotChargeRuleUrl: '/client/parkingInfo/feeRule/',
+  // 获取附近的停车场
+  nearParkingLotUrl: '/client/parkingInfo/pageList',
+  // 获取停车场收费规则
+  parkingLotChargeRuleUrl: '/client/parkingInfo/feeRule/',
   //包月信息
   monthInfoUrl: '/client/memberinfo/monthInfo',
   //创建包月
@@ -59,8 +59,8 @@ const apiurl = {
   getMonthPayUrl: '/client/monthpay/query/',
   //包月列表
   getMonthListUrl: '/client/memberinfo/monthList',
-	// 停车场包月列表
-	getParkMonthListUrl: '/client/memberinfo/monthList',
+  // 停车场包月列表
+  getParkMonthListUrl: '/client/memberinfo/monthList',
   //取消包月订单
   cancelMonthUrl: '/client/memberinfo/cancelMonth/',
 
@@ -125,8 +125,12 @@ const apiurl = {
   getParamsUrl: '/admin/config/configKey/',
   // 微信包月支付
   wechatMonthlyPayUrl: '/client/monthpay/wechat',
-	// 包月规则详情
-	monthlyRuleDetailsUrl: '/admin/feemonthrule',
+  // 包月规则详情
+  monthlyRuleDetailsUrl: '/admin/feemonthrule',
+  // 查询优惠券列表
+  couponListUrl: '/client/couponInfo/couponList',
+  // 兑换优惠券
+  exchangeCouponUrl: '/client/couponInfo/couponExchange'
 };
 
 export { apiurl };

+ 5 - 1
common/http.api.js

@@ -116,6 +116,8 @@ const install = (Vue, vm) => {
   let getParamsApi = (params = {}) => vm.$u.get(apiurl.getParamsUrl + params.key);
   let wechatMonthlyPayapi = (params = {}) => vm.$u.post(apiurl.wechatMonthlyPayUrl, params);
 	let monthlyRuleDetailsApi = (params = {}) => vm.$u.get(apiurl.monthlyRuleDetailsUrl, params);
+	let couponListApi = (params = {}) => vm.$u.get(apiurl.couponListUrl, params);
+  let exchangeCouponApi = (params = {}) => vm.$u.put(apiurl.exchangeCouponUrl, params);
   // 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
   vm.$u.api = {
     feedbackAdd,
@@ -177,7 +179,9 @@ const install = (Vue, vm) => {
     parkingWechatPayApi,
     getParamsApi,
     wechatMonthlyPayapi,
-		monthlyRuleDetailsApi
+		monthlyRuleDetailsApi,
+		couponListApi,
+    exchangeCouponApi
   };
 };
 

+ 226 - 0
components/swiper-list-item/swiper-list-item.vue

@@ -0,0 +1,226 @@
+<!-- 在这个文件对每个tab对应的列表进行渲染 -->
+<template>
+  <view class="content">
+    <z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
+      <view class="coupon-list-item" v-for="(item, index) in dataList" :key="index">
+        <view class="coupon-list-item-top">
+          <view class="clit-left">
+            <view class="clit-left-top"
+              >¥<text>{{ item.couponContent }}</text></view
+            >
+            <view class="clit-left-bottom">{{ Number(item.threshold) > 0 ? `停车时长满${item.threshold}分钟` : '无门槛' }}</view>
+          </view>
+          <view class="clit-center">
+            <view>{{ item.couponName }}</view>
+            <view>{{ item.vehicleNo }}</view>
+          </view>
+          <template v-if="Number(item.status) === 0">
+            <view class="clit-right">
+              <u-button class="clit-right-btn" type="primary" size="default" @click="goToUse(item)">立即使用</u-button>
+            </view>
+          </template>
+          <template v-else-if="Number(item.status) === 1">
+            <view class="clit-right">
+              <u-image width="128rpx" height="128rpx" src="/static/img/have-used-icon.svg"/>
+            </view>
+          </template>
+          <template v-else-if="Number(item.status) === 2">
+            <view class="clit-right">
+              <u-image width="128rpx" height="128rpx" src="/static/img/have-overdued-icon.svg"/>
+            </view>
+          </template>
+        </view>
+        <view class="coupon-list-item-bottom">
+          <view class="coupon-list-item-bottom-left">
+            适用停车点:
+            <template v-if="item.parkList.length > 1"> 适用多个停车点 </template>
+            <template v-else> 仅限{{ item.parkList.map((item) => item.parkName).join('、') }} </template>
+          </view>
+          <template v-if="Number(item.status) === 0">
+            <view class="coupon-list-item-bottom-right">有效期:{{ calcValidity(item.startTime, item.endTime) }} </view>
+          </template>
+          <template v-else>
+            <view class="coupon-list-item-bottom-right">已失效</view>
+          </template>
+        </view>
+        <template v-if="item.parkList.length > 1">
+          <view class="clibl-point">
+            <u-read-more
+              text-indent="0"
+              show-height="0"
+              :toggle="true"
+              :shadow-style="{ backgroundImage: 'none' }"
+              fontSize="20rpx"
+              close-text="展开所有停车点"
+            >
+              <view class="clibl-point-content">{{ item.parkList.map((item) => item.parkName).join('、') }}</view>
+            </u-read-more>
+          </view>
+        </template>
+      </view>
+    </z-paging>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      dataList: [],
+      firstLoaded: false
+    };
+  },
+  props: {
+    tabIndex: {
+      type: Number,
+      default: function () {
+        return 0;
+      }
+    },
+    currentIndex: {
+      type: Number,
+      default: function () {
+        return 0;
+      }
+    }
+  },
+  watch: {
+    currentIndex: {
+      handler(newVal) {
+        if (newVal === this.tabIndex) {
+          //懒加载,当滑动到当前的item时,才去加载
+          if (!this.firstLoaded) {
+            setTimeout(() => {
+              this.$refs.paging.reload();
+            }, 100);
+          }
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    queryList(pageNo, pageSize) {
+      const params = {
+        pageNo: pageNo,
+        pageSize: pageSize,
+        status: this.currentIndex
+      };
+      this.$u.api
+        .couponListApi(params)
+        .then((res) => {
+          this.$refs.paging.complete(res.data.rows);
+          this.firstLoaded = true;
+        })
+        .catch((res) => {
+          this.$refs.paging.complete(false);
+        });
+    },
+    /**
+     * 重载数据
+     * @date 2022-12-23
+     * @returns {any}
+     */
+    reloadData() {
+      this.$refs.paging.reload();
+    },
+    goToUse(item) {
+      this.$u.route({
+        url: 'pages/payLists/payLists'
+      });
+    },
+    /**
+     * 计算剩余时间
+     * @date 2022-12-23
+     * @param {any} datetime
+     * @returns {any}
+     */
+    calcValidity(startTime, endTime) {
+      let endTimeStr = new Date(endTime).valueOf(),
+        nowTimeStr = new Date(startTime).valueOf() < Date.now() ? Date.now() : new Date(startTime).valueOf(),
+        remainTimeStr = endTimeStr - nowTimeStr,
+        day = Math.floor(remainTimeStr / (1000 * 3600 * 24)),
+        dayOver = remainTimeStr % (1000 * 3600 * 24),
+        hours = Math.floor(dayOver / (3600 * 1000)),
+        hourOver = dayOver % (3600 * 1000),
+        minutes = Math.floor(hourOver / (60 * 1000));
+      return `${day}天${hours}小时${minutes}分`;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+/* 注意:父节点需要固定高度,z-paging的height:100%才会生效 */
+.content {
+  height: 100%;
+}
+.coupon-list-item {
+  background-color: #fff;
+  border-radius: 17rpx;
+  box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.08);
+  padding: 20rpx 0;
+  margin-bottom: 30rpx;
+  &-top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 26rpx 30rpx 37rpx;
+    border-bottom: 1px solid #eeeeee;
+    .clit-left {
+      color: #ff6d6d;
+      font-size: 24rpx;
+      width: 30%;
+      &-top {
+        margin-bottom: 10rpx;
+        text {
+          font-size: 60rpx;
+        }
+      }
+      &-bottom {
+        width: 100%;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+    .clit-center {
+      font-size: 20rpx;
+      color: #333;
+      width: 40%;
+      margin-left: 2%;
+      view:first-child {
+        width: 100%;
+        font-size: 32rpx;
+        color: #666;
+        font-weight: 500;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-bottom: 16rpx;
+      }
+    }
+    .clit-right {
+      margin-left: 2%;
+      &-btn {
+        background-color: #ff6d6d;
+      }
+    }
+  }
+  &-bottom {
+    font-size: 20rpx;
+    color: #999999;
+    display: flex;
+    justify-content: space-between;
+    padding: 10rpx 30rpx 0;
+  }
+  .clibl-point {
+    margin-top: 20rpx;
+    font-size: 22rpx;
+    &-content {
+      font-size: 22rpx;
+      padding: 0 30rpx;
+    }
+  }
+}
+</style>

+ 342 - 328
pages.json

@@ -1,330 +1,344 @@
 {
-  "easycom": {
-    "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
-  },
-  "pages": [
-    {
-      "path": "pages/index/index",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "城市智慧停车"
-      }
-    },
-    {
-      "path": "pages/center/index",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTextStyle": "white",
-        "navigationBarTitleText": "城市智慧停车"
-      }
-    },
-    {
-      "path": "pages/center/phoneLogin/phoneLogin",
-      "style": {
-        "navigationBarTitleText": "手机号登录",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/center/order/order",
-      "style": {
-        "navigationBarTitleText": "停车记录",
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/center/order/orderDetails/orderDetails",
-      "style": {
-        "navigationBarTitleText": "订单详情"
-      }
-    },
-    {
-      "path": "pages/center/invoice/invoice",
-      "style": {
-        "navigationBarTitleText": "我的发票"
-      }
-    },
-    {
-      "path": "pages/center/invoice/makeinvoice/makeinvoice",
-      "style": {
-        "navigationBarTitleText": "我要开票"
-      }
-    },
-    {
-      "path": "pages/center/invoice/invoiceDetails/invoiceDetails",
-      "style": {
-        "navigationBarTitleText": "发票详情"
-      }
-    },
-    {
-      "path": "pages/parkingLists/parkingLists",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "停车点"
-      }
-    },
-    {
-      "path": "pages/myCars/myCars",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "我的车辆"
-      }
-    },
-    {
-      "path": "pages/message/message",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "消息中心"
-      }
-    },
-    {
-      "path": "pages/message/messageInfo",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "消息中心"
-      }
-    },
-    {
-      "path": "pages/payLists/payLists",
-      "style": {
-        "navigationBarTitleText": "停车缴费",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkingInformation/parkingInformation",
-      "style": {
-        "navigationBarTitleText": "停车场信息",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#ffffff",
-        "navigationBarTextStyle": "black"
-      }
-    },
-    {
-      "path": "pages/chargeStandard/chargeStandard",
-      "style": {
-        "navigationBarTitleText": "收费标准",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/handleMonthly/handleMonthly",
-      "style": {
-        "navigationBarTitleText": "办理包月",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/center/monthly/monthly1",
-      "style": {
-        "navigationBarTitleText": "我的包月",
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/center/monthly/monthly",
-      "style": {
-        "navigationBarTitleText": "我的包月",
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/payLists/pay",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "支付"
-      }
-    },
-    {
-      "path": "pages/paymentMethod/jumpMiddle",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "跳转"
-      }
-    },
-    {
-      "path": "pages/handleMonthly/monthPay",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "包月支付"
-      }
-    },
-    {
-      "path": "pages/searchparking/searchparking",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "停车查询"
-      }
-    },
-    {
-      "path": "pages/favourableActivity/favourableActivity",
-      "style": {
-        "navigationBarTitleText": "一分钱停车",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkingLock/parkingLock",
-      "style": {
-        "navigationBarTitleText": "车位锁",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkingLists/map_web_view/map_web_view",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "导航",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/bannerDetails/bannerDetails",
-      "style": {
-        "navigationBarTitleText": "详情页",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/applyRefund/applyRefund",
-      "style": {
-        "navigationBarTitleText": "申请退款",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/applyRefundDetails/applyRefundDetails",
-      "style": {
-        "navigationBarTitleText": "申请退款",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/applyRefundDetails/applyRefundAchieveDetails",
-      "style": {
-        "navigationBarTitleText": "订单详情"
-      }
-    },
-    {
-      "path": "pages/privacyPolicy/privacyPolicy",
-      "style": {
-        "navigationBarTitleText": "隐私政策",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/wechatLogin/wechatLogin",
-      "style": {
-        "navigationBarTitleText": "微信登录",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/geomagnetismLock/geomagnetismLock",
-      "style": {
-        "navigationBarTitleText": "地磁",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkentrace/parkentrace",
-      "style": {
-        "navigationBarTitleText": "入口扫码",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkadvance/parkadvance",
-      "style": {
-        "navigationBarTitleText": "场内扫码",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkexport/parkexport",
-      "style": {
-        "navigationBarTitleText": "出口扫码",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/parkroadgate/parkroadgate",
-      "style": {
-        "navigationBarTitleText": "道闸",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    },
-    {
-      "path": "pages/roadGateSystem/roadGateSystem",
-      "style": {
-        "navigationBarTitleText": "道闸系统",
-        "enablePullDownRefresh": false,
-        "navigationBarBackgroundColor": "#008CFF",
-        "navigationBarTextStyle": "white"
-      }
-    }
-  ],
-  "globalStyle": {
-    "navigationBarTextStyle": "black",
-    "navigationBarTitleText": "普定智慧停车",
-    "navigationBarBackgroundColor": "#F8F8F8",
-    "backgroundColor": "#F8F8F8",
-    "backgroundColorTop": "#FFFFFF"
-  },
-  "tabBar": {
-    "color": "#909399",
-    "selectedColor": "#303133",
-    "borderStyle": "black",
-    "backgroundColor": "#ffffff",
-    "list": [
-      {
-        "pagePath": "pages/index/index",
-        "iconPath": "static/index.png",
-        "selectedIconPath": "static/index-selected.png",
-        "text": "首页"
-      },
-      {
-        "pagePath": "pages/parkingLists/parkingLists",
-        "iconPath": "static/car_h.png",
-        "selectedIconPath": "static/car.png",
-        "text": "找车位"
-      },
-      {
-        "pagePath": "pages/center/index",
-        "iconPath": "static/center.png",
-        "selectedIconPath": "static/center-selected.png",
-        "text": "我"
-      }
-    ]
-  }
+	"easycom": {
+		"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	"pages": [{
+			"path": "pages/index/index",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "城市智慧停车"
+			}
+		},
+		{
+			"path": "pages/center/index",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "white",
+				"navigationBarTitleText": "城市智慧停车"
+			}
+		},
+		{
+			"path": "pages/center/phoneLogin/phoneLogin",
+			"style": {
+				"navigationBarTitleText": "手机号登录",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/center/order/order",
+			"style": {
+				"navigationBarTitleText": "停车记录",
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/center/order/orderDetails/orderDetails",
+			"style": {
+				"navigationBarTitleText": "订单详情"
+			}
+		},
+		{
+			"path": "pages/center/invoice/invoice",
+			"style": {
+				"navigationBarTitleText": "我的发票"
+			}
+		},
+		{
+			"path": "pages/center/invoice/makeinvoice/makeinvoice",
+			"style": {
+				"navigationBarTitleText": "我要开票"
+			}
+		},
+		{
+			"path": "pages/center/invoice/invoiceDetails/invoiceDetails",
+			"style": {
+				"navigationBarTitleText": "发票详情"
+			}
+		},
+		{
+			"path": "pages/parkingLists/parkingLists",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "停车点"
+			}
+		},
+		{
+			"path": "pages/myCars/myCars",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "我的车辆"
+			}
+		},
+		{
+			"path": "pages/message/message",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "消息中心"
+			}
+		},
+		{
+			"path": "pages/message/messageInfo",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "消息中心"
+			}
+		},
+		{
+			"path": "pages/payLists/payLists",
+			"style": {
+				"navigationBarTitleText": "停车缴费",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkingInformation/parkingInformation",
+			"style": {
+				"navigationBarTitleText": "停车场信息",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#ffffff",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/chargeStandard/chargeStandard",
+			"style": {
+				"navigationBarTitleText": "收费标准",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/handleMonthly/handleMonthly",
+			"style": {
+				"navigationBarTitleText": "办理包月",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/center/monthly/monthly1",
+			"style": {
+				"navigationBarTitleText": "我的包月",
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/center/monthly/monthly",
+			"style": {
+				"navigationBarTitleText": "我的包月",
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/payLists/pay",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "支付"
+			}
+		},
+		{
+			"path": "pages/paymentMethod/jumpMiddle",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "跳转"
+			}
+		},
+		{
+			"path": "pages/handleMonthly/monthPay",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "包月支付"
+			}
+		},
+		{
+			"path": "pages/searchparking/searchparking",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "停车查询"
+			}
+		},
+		{
+			"path": "pages/favourableActivity/favourableActivity",
+			"style": {
+				"navigationBarTitleText": "一分钱停车",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkingLock/parkingLock",
+			"style": {
+				"navigationBarTitleText": "车位锁",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkingLists/map_web_view/map_web_view",
+			"style": {
+				"navigationStyle": "custom",
+				"navigationBarTitleText": "导航",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/bannerDetails/bannerDetails",
+			"style": {
+				"navigationBarTitleText": "详情页",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/applyRefund/applyRefund",
+			"style": {
+				"navigationBarTitleText": "申请退款",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/applyRefundDetails/applyRefundDetails",
+			"style": {
+				"navigationBarTitleText": "申请退款",
+				"navigationStyle": "custom",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/applyRefundDetails/applyRefundAchieveDetails",
+			"style": {
+				"navigationBarTitleText": "订单详情"
+			}
+		},
+		{
+			"path": "pages/privacyPolicy/privacyPolicy",
+			"style": {
+				"navigationBarTitleText": "隐私政策",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/wechatLogin/wechatLogin",
+			"style": {
+				"navigationBarTitleText": "微信登录",
+				"enablePullDownRefresh": false
+			}
+		},
+		{
+			"path": "pages/geomagnetismLock/geomagnetismLock",
+			"style": {
+				"navigationBarTitleText": "地磁",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkentrace/parkentrace",
+			"style": {
+				"navigationBarTitleText": "入口扫码",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkadvance/parkadvance",
+			"style": {
+				"navigationBarTitleText": "场内扫码",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkexport/parkexport",
+			"style": {
+				"navigationBarTitleText": "出口扫码",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/parkroadgate/parkroadgate",
+			"style": {
+				"navigationBarTitleText": "道闸",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/roadGateSystem/roadGateSystem",
+			"style": {
+				"navigationBarTitleText": "道闸系统",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+		}, {
+			"path": "pages/center/coupon/myCoupon/myCoupon",
+			"style": {
+				"navigationBarTitleText": "我的优惠券",
+				"enablePullDownRefresh": false,
+				"navigationStyle": "custom"
+			}
+		}, {
+			"path": "pages/center/coupon/couponRules/couponRules",
+			"style": {
+				"navigationBarTitleText": "优惠券说明",
+				"enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#008CFF",
+				"navigationBarTextStyle": "white"
+			}
+
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "普定智慧停车",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8",
+		"backgroundColorTop": "#FFFFFF"
+	},
+	"tabBar": {
+		"color": "#909399",
+		"selectedColor": "#303133",
+		"borderStyle": "black",
+		"backgroundColor": "#ffffff",
+		"list": [{
+				"pagePath": "pages/index/index",
+				"iconPath": "static/index.png",
+				"selectedIconPath": "static/index-selected.png",
+				"text": "首页"
+			},
+			{
+				"pagePath": "pages/parkingLists/parkingLists",
+				"iconPath": "static/car_h.png",
+				"selectedIconPath": "static/car.png",
+				"text": "找车位"
+			},
+			{
+				"pagePath": "pages/center/index",
+				"iconPath": "static/center.png",
+				"selectedIconPath": "static/center-selected.png",
+				"text": "我"
+			}
+		]
+	}
 }

+ 35 - 0
pages/center/coupon/couponRules/couponRules.vue

@@ -0,0 +1,35 @@
+<template>
+  <view class="content ql-editor">
+		<u-parse :html="details.content"></u-parse>
+	</view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+			details: {}
+		};
+  },
+  onLoad(options) {
+    this.getExplain();
+  },
+  methods: {
+    getExplain() {
+      this.$u.api
+        .getSysterms({
+          termsType: 5
+        })
+        .then((res) => {
+					this.details = res.data
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+	padding: 30rpx;
+}
+</style>

+ 26 - 0
pages/center/coupon/myCoupon/myCoupon.scss

@@ -0,0 +1,26 @@
+.navbar-font {
+  color: #fff;
+  padding-right: 20rpx;
+}
+.swiper {
+  height: 100%;
+	padding: 0 30rpx;
+  background-color: #f9f9f9;
+}
+.content {
+	height: calc(100% - 140rpx);
+}
+.search-part {
+	display: flex;
+	padding: 30rpx 0 0;
+	margin-bottom: 30rpx;
+	&-input {
+		background-color: #fff;
+		border-radius: 32rpx;
+		padding-left: 30rpx!important;
+		margin-right: 30rpx;
+	}
+	&-btn {
+		background-color: #FF6D6D;
+	}
+}

+ 146 - 0
pages/center/coupon/myCoupon/myCoupon.vue

@@ -0,0 +1,146 @@
+<template>
+  <view class="content">
+    <z-paging-swiper>
+      <u-navbar
+        slot="top"
+        title-color="#fff"
+        :custom-back="customBack"
+        :border-bottom="false"
+        back-icon-color="#CCE8FF"
+        :background="{ background: '#008CFF' }"
+        title="我的优惠券"
+      >
+        <view class="navbar-font" slot="right" @click="jumpPage('/pages/center/coupon/couponRules/couponRules')"> 规则 </view>
+      </u-navbar>
+      <z-tabs ref="tabs" slot="top" :list="tabList" :current="current" barWidth="90rpx" @change="tabsChange"></z-tabs>
+      <swiper class="swiper" :current="current" @transition="swiperTransition" @animationfinish="swiperAnimationfinish">
+        <swiper-item class="swiper-item" v-for="(item, index) in tabList" :key="index">
+          <view class="search-part" v-if="current === 0">
+            <u-input class="search-part-input" v-model="form.exchangeCode" placeholder="请输入兑换码" />
+            <u-button class="search-part-btn" type="primary" shape="circle" size="medium" @click="handleExchange">兑换</u-button>
+          </view>
+          <swiper-list-item :tabIndex="index" :currentIndex="current" ref="swiperListItem"></swiper-list-item>
+        </swiper-item>
+      </swiper>
+      <!-- 选择车牌 -->
+      <u-select v-model="chooseVehicle" :list="vehicleList" @confirm="vehicleConfirm"></u-select>
+      <u-toast ref="uToast" />
+    </z-paging-swiper>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      tabList: [
+        {
+          name: '未使用',
+          value: 0,
+          list: []
+        },
+        {
+          name: '已使用',
+          value: 1,
+          list: []
+        },
+        {
+          name: '已失效',
+          value: 2,
+          list: []
+        }
+      ],
+      current: 0,
+      form: {
+        exchangeCode: '',
+        source: 1,
+        vehicleNo: '京A88888'
+      },
+      vehicleList: [],
+      chooseVehicle: false
+    };
+  },
+  onLoad(options) {
+    this.getCarsList();
+  },
+  methods: {
+    // tabs通知swiper切换
+    tabsChange(index) {
+      this.current = index;
+    },
+    // swiper滑动中
+    swiperTransition(e) {
+      this.$refs.tabs.setDx(e.detail.dx);
+    },
+    //swiper滑动结束
+    swiperAnimationfinish(e) {
+      this.current = e.detail.current;
+      this.$refs.tabs.unlockDx();
+    },
+    /**
+     * 兑换
+     * @date 2022-12-23
+     * @returns {any}
+     */
+    handleExchange() {
+      if (this.form.exchangeCode) {
+        this.chooseVehicle = true;
+      } else {
+        this.$refs.uToast.show({
+          title: '请输入兑换码',
+          type: 'warning'
+        });
+      }
+    },
+    getCarsList() {
+      this.$u.api.getMycars().then((res) => {
+        this.vehicleList = res.data.rows.map((item) => {
+          return { label: item.vehicleNo, value: item.vehicleNo };
+        });
+      });
+    },
+    /**
+     * 车牌确认
+     * @date 2022-12-23
+     * @param {any} list
+     * @returns {any}
+     */
+    vehicleConfirm(list) {
+      this.form.vehicleNo = list[0].value;
+      this.exchangeCoupon();
+    },
+    /**
+     * 兑换优惠券
+     * @date 2022-12-23
+     * @returns {any}
+     */
+    exchangeCoupon() {
+      this.$u.api.exchangeCouponApi(this.form).then((res) => {
+        if (res.code === 200) {
+          this.$refs.uToast.show({
+            title: '兑换成功!',
+            type: 'success'
+          });
+          this.$refs['swiperListItem'][this.current].reloadData();
+        }
+      });
+    },
+    jumpPage(url) {
+      this.$u.route({
+        url
+      });
+    },
+    // tabbar 返回
+    customBack() {
+      this.$u.route({
+        type: 'switchTab',
+        url: 'pages/index/index'
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import './myCoupon.scss';
+</style>

+ 3 - 3
pages/center/index.vue

@@ -87,11 +87,11 @@
     </view>
     <u-select v-model="callPhoneShow" :list="callPhoneList" @confirm="phoneCall"></u-select>
 
-    <!-- <view class="u-m-t-20">
+    <view class="u-m-t-20">
 			<u-cell-group>
-				<u-cell-item icon="phone" title="手机号登录" @click="openPage('/pages/center/phoneLogin/phoneLogin')"></u-cell-item>
+				<u-cell-item icon="coupon" title="我的优惠券" @click="openPage('/pages/center/coupon/myCoupon/myCoupon')"></u-cell-item>
 			</u-cell-group>
-    </view>-->
+    </view>
 
     <!-- ===================================== 登出提示 ===================================== -->
     <u-modal

+ 27 - 0
static/img/have-overdued-icon.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="优惠券备份-3" transform="translate(-576.000000, -419.000000)">
+            <g id="编组-7" transform="translate(30.000000, 399.000000)">
+                <g id="编组-5" transform="translate(610.000000, 84.000000) rotate(15.000000) translate(-610.000000, -84.000000) translate(560.000000, 34.000000)">
+                    <g id="编组-6" transform="translate(20.441481, 10.982576)" fill="#CCCCCC" fill-rule="nonzero">
+                        <polygon id="路径" points="40.4275464 6.52138157 42.8874406 7.62816666 42.8478751 4.99205866 44.7508589 3.05692427 42.1352594 2.33254061 40.8514702 0.0298213522 39.2746062 2.21855026 36.577493 2.7303422 38.2188601 4.807317 37.8358075 7.4264357"></polygon>
+                        <polygon id="路径" points="3.92705276 10.3785248 1.46064494 9.4796197 1.69783777 12.1776733 2.75525374e-17 14.3434509 2.58868438 14.835573 4.0051918 17.0732614 5.36795029 14.6789128 7.94147821 13.8961681 6.19515578 11.9244119 6.36885188 9.20288225"></polygon>
+                        <polygon id="路径" points="22.4122038 1.60979415 20.3315029 0 19.7424492 2.64008485 17.4690506 4.1816832 19.7890915 5.44158567 20.4646878 8.00430935 22.487434 6.14275659 25.1785065 6.18475626 24.1086767 3.77440086 25.0964577 1.23807701"></polygon>
+                        <polygon id="路径" points="54.8095845 9.97248372 52.2221894 9.03678943 52.5929383 11.6608065 50.9422096 13.7178147 53.6366731 14.2616659 55.203618 16.4690166 56.4976529 14.1816384 59.1170376 13.4884088 57.222676 11.5305713 57.2743957 8.89485874"></polygon>
+                        <polygon id="路径备份" transform="translate(40.664176, 74.205854) scale(1, -1) translate(-40.664176, -74.205854) " points="40.4275464 76.8982413 42.8874406 78.0050264 42.8478751 75.3689184 44.7508589 73.433784 42.1352594 72.7094004 40.8514702 70.4066811 39.2746062 72.59541 36.577493 73.1072019 38.2188601 75.1841767 37.8358075 77.8032954"></polygon>
+                        <polygon id="路径备份-2" transform="translate(3.970739, 64.896776) scale(1, -1) translate(-3.970739, -64.896776) " points="3.92705276 62.1372289 1.46064494 61.2383238 1.69783777 63.9363774 2.75525374e-17 66.102155 2.58868438 66.5942771 4.0051918 68.8319655 5.36795029 66.4376168 7.94147821 65.6548722 6.19515578 63.683116 6.36885188 60.9615863"></polygon>
+                        <polygon id="路径备份-3" transform="translate(21.323779, 74.032693) scale(1, -1) translate(-21.323779, -74.032693) " points="22.4122038 71.6403326 20.3315029 70.0305384 19.7424492 72.6706232 17.4690506 74.2122216 19.7890915 75.4721241 20.4646878 78.0348478 22.487434 76.173295 25.1785065 76.2152947 24.1086767 73.8049393 25.0964577 71.2686154"></polygon>
+                        <polygon id="路径备份-4" transform="translate(55.029624, 65.352910) scale(1, -1) translate(-55.029624, -65.352910) " points="54.8095845 62.6434561 52.2221894 61.7077618 52.5929383 64.3317789 50.9422096 66.3887871 53.6366731 66.9326383 55.203618 69.139989 56.4976529 66.8526108 59.1170376 66.1593812 57.222676 64.2015437 57.2743957 61.5658311"></polygon>
+                    </g>
+                    <circle id="椭圆形" stroke="#CCCCCC" stroke-width="3" cx="50" cy="50" r="50"></circle>
+                    <circle id="椭圆形" stroke="#CCCCCC" cx="50" cy="50" r="44"></circle>
+                    <text id="已过期" font-family="PingFangSC-Semibold, PingFang SC" font-size="24" font-weight="500" fill="#CCCCCC">
+                        <tspan x="14" y="58.5">已过期</tspan>
+                    </text>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 27 - 0
static/img/have-used-icon.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="优惠券备份-2" transform="translate(-576.000000, -419.000000)">
+            <g id="编组-7" transform="translate(30.000000, 399.000000)">
+                <g id="编组-5" transform="translate(610.000000, 84.000000) rotate(15.000000) translate(-610.000000, -84.000000) translate(560.000000, 34.000000)">
+                    <g id="编组-6" transform="translate(20.441481, 10.982576)" fill="#CCCCCC" fill-rule="nonzero">
+                        <polygon id="路径" points="40.4275464 6.52138157 42.8874406 7.62816666 42.8478751 4.99205866 44.7508589 3.05692427 42.1352594 2.33254061 40.8514702 0.0298213522 39.2746062 2.21855026 36.577493 2.7303422 38.2188601 4.807317 37.8358075 7.4264357"></polygon>
+                        <polygon id="路径" points="3.92705276 10.3785248 1.46064494 9.4796197 1.69783777 12.1776733 2.75525374e-17 14.3434509 2.58868438 14.835573 4.0051918 17.0732614 5.36795029 14.6789128 7.94147821 13.8961681 6.19515578 11.9244119 6.36885188 9.20288225"></polygon>
+                        <polygon id="路径" points="22.4122038 1.60979415 20.3315029 0 19.7424492 2.64008485 17.4690506 4.1816832 19.7890915 5.44158567 20.4646878 8.00430935 22.487434 6.14275659 25.1785065 6.18475626 24.1086767 3.77440086 25.0964577 1.23807701"></polygon>
+                        <polygon id="路径" points="54.8095845 9.97248372 52.2221894 9.03678943 52.5929383 11.6608065 50.9422096 13.7178147 53.6366731 14.2616659 55.203618 16.4690166 56.4976529 14.1816384 59.1170376 13.4884088 57.222676 11.5305713 57.2743957 8.89485874"></polygon>
+                        <polygon id="路径备份" transform="translate(40.664176, 74.205854) scale(1, -1) translate(-40.664176, -74.205854) " points="40.4275464 76.8982413 42.8874406 78.0050264 42.8478751 75.3689184 44.7508589 73.433784 42.1352594 72.7094004 40.8514702 70.4066811 39.2746062 72.59541 36.577493 73.1072019 38.2188601 75.1841767 37.8358075 77.8032954"></polygon>
+                        <polygon id="路径备份-2" transform="translate(3.970739, 64.896776) scale(1, -1) translate(-3.970739, -64.896776) " points="3.92705276 62.1372289 1.46064494 61.2383238 1.69783777 63.9363774 2.75525374e-17 66.102155 2.58868438 66.5942771 4.0051918 68.8319655 5.36795029 66.4376168 7.94147821 65.6548722 6.19515578 63.683116 6.36885188 60.9615863"></polygon>
+                        <polygon id="路径备份-3" transform="translate(21.323779, 74.032693) scale(1, -1) translate(-21.323779, -74.032693) " points="22.4122038 71.6403326 20.3315029 70.0305384 19.7424492 72.6706232 17.4690506 74.2122216 19.7890915 75.4721241 20.4646878 78.0348478 22.487434 76.173295 25.1785065 76.2152947 24.1086767 73.8049393 25.0964577 71.2686154"></polygon>
+                        <polygon id="路径备份-4" transform="translate(55.029624, 65.352910) scale(1, -1) translate(-55.029624, -65.352910) " points="54.8095845 62.6434561 52.2221894 61.7077618 52.5929383 64.3317789 50.9422096 66.3887871 53.6366731 66.9326383 55.203618 69.139989 56.4976529 66.8526108 59.1170376 66.1593812 57.222676 64.2015437 57.2743957 61.5658311"></polygon>
+                    </g>
+                    <circle id="椭圆形" stroke="#CCCCCC" stroke-width="3" cx="50" cy="50" r="50"></circle>
+                    <circle id="椭圆形" stroke="#CCCCCC" cx="50" cy="50" r="44"></circle>
+                    <text id="已使用" font-family="PingFangSC-Semibold, PingFang SC" font-size="24" font-weight="500" fill="#CCCCCC">
+                        <tspan x="14" y="58.5">已使用</tspan>
+                    </text>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 952 - 0
static/quill/quill.bubble.scss

@@ -0,0 +1,952 @@
+/*!
+ * Quill Editor v1.3.7
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}
+.ql-bubble.ql-toolbar:after,
+.ql-bubble .ql-toolbar:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-bubble.ql-toolbar button,
+.ql-bubble .ql-toolbar button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  display: inline-block;
+  float: left;
+  height: 24px;
+  padding: 3px 5px;
+  width: 28px;
+}
+.ql-bubble.ql-toolbar button svg,
+.ql-bubble .ql-toolbar button svg {
+  float: left;
+  height: 100%;
+}
+.ql-bubble.ql-toolbar button:active:hover,
+.ql-bubble .ql-toolbar button:active:hover {
+  outline: none;
+}
+.ql-bubble.ql-toolbar input.ql-image[type=file],
+.ql-bubble .ql-toolbar input.ql-image[type=file] {
+  display: none;
+}
+.ql-bubble.ql-toolbar button:hover,
+.ql-bubble .ql-toolbar button:hover,
+.ql-bubble.ql-toolbar button:focus,
+.ql-bubble .ql-toolbar button:focus,
+.ql-bubble.ql-toolbar button.ql-active,
+.ql-bubble .ql-toolbar button.ql-active,
+.ql-bubble.ql-toolbar .ql-picker-label:hover,
+.ql-bubble .ql-toolbar .ql-picker-label:hover,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active,
+.ql-bubble.ql-toolbar .ql-picker-item:hover,
+.ql-bubble .ql-toolbar .ql-picker-item:hover,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected {
+  color: #fff;
+}
+.ql-bubble.ql-toolbar button:hover .ql-fill,
+.ql-bubble .ql-toolbar button:hover .ql-fill,
+.ql-bubble.ql-toolbar button:focus .ql-fill,
+.ql-bubble .ql-toolbar button:focus .ql-fill,
+.ql-bubble.ql-toolbar button.ql-active .ql-fill,
+.ql-bubble .ql-toolbar button.ql-active .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
+  fill: #fff;
+}
+.ql-bubble.ql-toolbar button:hover .ql-stroke,
+.ql-bubble .ql-toolbar button:hover .ql-stroke,
+.ql-bubble.ql-toolbar button:focus .ql-stroke,
+.ql-bubble .ql-toolbar button:focus .ql-stroke,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-bubble.ql-toolbar button:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar button:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar button:focus .ql-stroke-miter,
+.ql-bubble .ql-toolbar button:focus .ql-stroke-miter,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
+  stroke: #fff;
+}
+@media (pointer: coarse) {
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active),
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) {
+    color: #ccc;
+  }
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
+    fill: #ccc;
+  }
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
+    stroke: #ccc;
+  }
+}
+.ql-bubble {
+  box-sizing: border-box;
+}
+.ql-bubble * {
+  box-sizing: border-box;
+}
+.ql-bubble .ql-hidden {
+  display: none;
+}
+.ql-bubble .ql-out-bottom,
+.ql-bubble .ql-out-top {
+  visibility: hidden;
+}
+.ql-bubble .ql-tooltip {
+  position: absolute;
+  transform: translateY(10px);
+}
+.ql-bubble .ql-tooltip a {
+  cursor: pointer;
+  text-decoration: none;
+}
+.ql-bubble .ql-tooltip.ql-flip {
+  transform: translateY(-10px);
+}
+.ql-bubble .ql-formats {
+  display: inline-block;
+  vertical-align: middle;
+}
+.ql-bubble .ql-formats:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-bubble .ql-stroke {
+  fill: none;
+  stroke: #ccc;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 2;
+}
+.ql-bubble .ql-stroke-miter {
+  fill: none;
+  stroke: #ccc;
+  stroke-miterlimit: 10;
+  stroke-width: 2;
+}
+.ql-bubble .ql-fill,
+.ql-bubble .ql-stroke.ql-fill {
+  fill: #ccc;
+}
+.ql-bubble .ql-empty {
+  fill: none;
+}
+.ql-bubble .ql-even {
+  fill-rule: evenodd;
+}
+.ql-bubble .ql-thin,
+.ql-bubble .ql-stroke.ql-thin {
+  stroke-width: 1;
+}
+.ql-bubble .ql-transparent {
+  opacity: 0.4;
+}
+.ql-bubble .ql-direction svg:last-child {
+  display: none;
+}
+.ql-bubble .ql-direction.ql-active svg:last-child {
+  display: inline;
+}
+.ql-bubble .ql-direction.ql-active svg:first-child {
+  display: none;
+}
+.ql-bubble .ql-editor h1 {
+  font-size: 2em;
+}
+.ql-bubble .ql-editor h2 {
+  font-size: 1.5em;
+}
+.ql-bubble .ql-editor h3 {
+  font-size: 1.17em;
+}
+.ql-bubble .ql-editor h4 {
+  font-size: 1em;
+}
+.ql-bubble .ql-editor h5 {
+  font-size: 0.83em;
+}
+.ql-bubble .ql-editor h6 {
+  font-size: 0.67em;
+}
+.ql-bubble .ql-editor a {
+  text-decoration: underline;
+}
+.ql-bubble .ql-editor blockquote {
+  border-left: 4px solid #ccc;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding-left: 16px;
+}
+.ql-bubble .ql-editor code,
+.ql-bubble .ql-editor pre {
+  background-color: #f0f0f0;
+  border-radius: 3px;
+}
+.ql-bubble .ql-editor pre {
+  white-space: pre-wrap;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding: 5px 10px;
+}
+.ql-bubble .ql-editor code {
+  font-size: 85%;
+  padding: 2px 4px;
+}
+.ql-bubble .ql-editor pre.ql-syntax {
+  background-color: #23241f;
+  color: #f8f8f2;
+  overflow: visible;
+}
+.ql-bubble .ql-editor img {
+  max-width: 100%;
+}
+.ql-bubble .ql-picker {
+  color: #ccc;
+  display: inline-block;
+  float: left;
+  font-size: 14px;
+  font-weight: 500;
+  height: 24px;
+  position: relative;
+  vertical-align: middle;
+}
+.ql-bubble .ql-picker-label {
+  cursor: pointer;
+  display: inline-block;
+  height: 100%;
+  padding-left: 8px;
+  padding-right: 2px;
+  position: relative;
+  width: 100%;
+}
+.ql-bubble .ql-picker-label::before {
+  display: inline-block;
+  line-height: 22px;
+}
+.ql-bubble .ql-picker-options {
+  background-color: #444;
+  display: none;
+  min-width: 100%;
+  padding: 4px 8px;
+  position: absolute;
+  white-space: nowrap;
+}
+.ql-bubble .ql-picker-options .ql-picker-item {
+  cursor: pointer;
+  display: block;
+  padding-bottom: 5px;
+  padding-top: 5px;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label {
+  color: #777;
+  z-index: 2;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill {
+  fill: #777;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
+  stroke: #777;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-options {
+  display: block;
+  margin-top: -1px;
+  top: 100%;
+  z-index: 1;
+}
+.ql-bubble .ql-color-picker,
+.ql-bubble .ql-icon-picker {
+  width: 28px;
+}
+.ql-bubble .ql-color-picker .ql-picker-label,
+.ql-bubble .ql-icon-picker .ql-picker-label {
+  padding: 2px 4px;
+}
+.ql-bubble .ql-color-picker .ql-picker-label svg,
+.ql-bubble .ql-icon-picker .ql-picker-label svg {
+  right: 4px;
+}
+.ql-bubble .ql-icon-picker .ql-picker-options {
+  padding: 4px 0px;
+}
+.ql-bubble .ql-icon-picker .ql-picker-item {
+  height: 24px;
+  width: 24px;
+  padding: 2px 4px;
+}
+.ql-bubble .ql-color-picker .ql-picker-options {
+  padding: 3px 5px;
+  width: 152px;
+}
+.ql-bubble .ql-color-picker .ql-picker-item {
+  border: 1px solid transparent;
+  float: left;
+  height: 16px;
+  margin: 2px;
+  padding: 0px;
+  width: 16px;
+}
+.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
+  position: absolute;
+  margin-top: -9px;
+  right: 0;
+  top: 50%;
+  width: 18px;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
+  content: attr(data-label);
+}
+.ql-bubble .ql-picker.ql-header {
+  width: 98px;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  content: 'Heading 1';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  content: 'Heading 2';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  content: 'Heading 3';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  content: 'Heading 4';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  content: 'Heading 5';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  content: 'Heading 6';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  font-size: 2em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  font-size: 1.5em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  font-size: 1.17em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  font-size: 1em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  font-size: 0.83em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  font-size: 0.67em;
+}
+.ql-bubble .ql-picker.ql-font {
+  width: 108px;
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item::before {
+  content: 'Sans Serif';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  content: 'Serif';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  content: 'Monospace';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-bubble .ql-picker.ql-size {
+  width: 98px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  content: 'Small';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  content: 'Large';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  content: 'Huge';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  font-size: 10px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  font-size: 18px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  font-size: 32px;
+}
+.ql-bubble .ql-color-picker.ql-background .ql-picker-item {
+  background-color: #fff;
+}
+.ql-bubble .ql-color-picker.ql-color .ql-picker-item {
+  background-color: #000;
+}
+.ql-bubble .ql-toolbar .ql-formats {
+  margin: 8px 12px 8px 0px;
+}
+.ql-bubble .ql-toolbar .ql-formats:first-child {
+  margin-left: 12px;
+}
+.ql-bubble .ql-color-picker svg {
+  margin: 1px;
+}
+.ql-bubble .ql-color-picker .ql-picker-item.ql-selected,
+.ql-bubble .ql-color-picker .ql-picker-item:hover {
+  border-color: #fff;
+}
+.ql-bubble .ql-tooltip {
+  background-color: #444;
+  border-radius: 25px;
+  color: #fff;
+}
+.ql-bubble .ql-tooltip-arrow {
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  content: " ";
+  display: block;
+  left: 50%;
+  margin-left: -6px;
+  position: absolute;
+}
+.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow {
+  border-bottom: 6px solid #444;
+  top: -6px;
+}
+.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow {
+  border-top: 6px solid #444;
+  bottom: -6px;
+}
+.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor {
+  display: block;
+}
+.ql-bubble .ql-tooltip.ql-editing .ql-formats {
+  visibility: hidden;
+}
+.ql-bubble .ql-tooltip-editor {
+  display: none;
+}
+.ql-bubble .ql-tooltip-editor input[type=text] {
+  background: transparent;
+  border: none;
+  color: #fff;
+  font-size: 13px;
+  height: 100%;
+  outline: none;
+  padding: 10px 20px;
+  position: absolute;
+  width: 100%;
+}
+.ql-bubble .ql-tooltip-editor a {
+  top: 10px;
+  position: absolute;
+  right: 20px;
+}
+.ql-bubble .ql-tooltip-editor a:before {
+  color: #ccc;
+  content: "\D7";
+  font-size: 16px;
+  font-weight: bold;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a {
+  position: relative;
+  white-space: nowrap;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::before {
+  background-color: #444;
+  border-radius: 15px;
+  top: -5px;
+  font-size: 12px;
+  color: #fff;
+  content: attr(href);
+  font-weight: normal;
+  overflow: hidden;
+  padding: 5px 15px;
+  text-decoration: none;
+  z-index: 1;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::after {
+  border-top: 6px solid #444;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  top: 0;
+  content: " ";
+  height: 0;
+  width: 0;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::before,
+.ql-container.ql-bubble:not(.ql-disabled) a::after {
+  left: 0;
+  margin-left: 50%;
+  position: absolute;
+  transform: translate(-50%, -100%);
+  transition: visibility 0s ease 200ms;
+  visibility: hidden;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a:hover::before,
+.ql-container.ql-bubble:not(.ql-disabled) a:hover::after {
+  visibility: visible;
+}

+ 399 - 0
static/quill/quill.core.scss

@@ -0,0 +1,399 @@
+/*!
+ * Quill Editor v1.3.7
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor /deep/ .ql-video {
+  display: block;
+  width: 100%;
+  margin-bottom: 24rpx;
+  video{width: 100%;}
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}

+ 945 - 0
static/quill/quill.snow.scss

@@ -0,0 +1,945 @@
+/*!
+ * Quill Editor v1.3.7
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}
+.ql-snow.ql-toolbar:after,
+.ql-snow .ql-toolbar:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-snow.ql-toolbar button,
+.ql-snow .ql-toolbar button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  display: inline-block;
+  float: left;
+  height: 24px;
+  padding: 3px 5px;
+  width: 28px;
+}
+.ql-snow.ql-toolbar button svg,
+.ql-snow .ql-toolbar button svg {
+  float: left;
+  height: 100%;
+}
+.ql-snow.ql-toolbar button:active:hover,
+.ql-snow .ql-toolbar button:active:hover {
+  outline: none;
+}
+.ql-snow.ql-toolbar input.ql-image[type=file],
+.ql-snow .ql-toolbar input.ql-image[type=file] {
+  display: none;
+}
+.ql-snow.ql-toolbar button:hover,
+.ql-snow .ql-toolbar button:hover,
+.ql-snow.ql-toolbar button:focus,
+.ql-snow .ql-toolbar button:focus,
+.ql-snow.ql-toolbar button.ql-active,
+.ql-snow .ql-toolbar button.ql-active,
+.ql-snow.ql-toolbar .ql-picker-label:hover,
+.ql-snow .ql-toolbar .ql-picker-label:hover,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active,
+.ql-snow.ql-toolbar .ql-picker-item:hover,
+.ql-snow .ql-toolbar .ql-picker-item:hover,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected {
+  color: #06c;
+}
+.ql-snow.ql-toolbar button:hover .ql-fill,
+.ql-snow .ql-toolbar button:hover .ql-fill,
+.ql-snow.ql-toolbar button:focus .ql-fill,
+.ql-snow .ql-toolbar button:focus .ql-fill,
+.ql-snow.ql-toolbar button.ql-active .ql-fill,
+.ql-snow .ql-toolbar button.ql-active .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
+  fill: #06c;
+}
+.ql-snow.ql-toolbar button:hover .ql-stroke,
+.ql-snow .ql-toolbar button:hover .ql-stroke,
+.ql-snow.ql-toolbar button:focus .ql-stroke,
+.ql-snow .ql-toolbar button:focus .ql-stroke,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar button:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar button:focus .ql-stroke-miter,
+.ql-snow .ql-toolbar button:focus .ql-stroke-miter,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
+  stroke: #06c;
+}
+@media (pointer: coarse) {
+  .ql-snow.ql-toolbar button:hover:not(.ql-active),
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) {
+    color: #444;
+  }
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
+    fill: #444;
+  }
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
+    stroke: #444;
+  }
+}
+.ql-snow {
+  box-sizing: border-box;
+}
+.ql-snow * {
+  box-sizing: border-box;
+}
+.ql-snow .ql-hidden {
+  display: none;
+}
+.ql-snow .ql-out-bottom,
+.ql-snow .ql-out-top {
+  visibility: hidden;
+}
+.ql-snow .ql-tooltip {
+  position: absolute;
+  transform: translateY(10px);
+}
+.ql-snow .ql-tooltip a {
+  cursor: pointer;
+  text-decoration: none;
+}
+.ql-snow .ql-tooltip.ql-flip {
+  transform: translateY(-10px);
+}
+.ql-snow .ql-formats {
+  display: inline-block;
+  vertical-align: middle;
+}
+.ql-snow .ql-formats:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-snow .ql-stroke {
+  fill: none;
+  stroke: #444;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 2;
+}
+.ql-snow .ql-stroke-miter {
+  fill: none;
+  stroke: #444;
+  stroke-miterlimit: 10;
+  stroke-width: 2;
+}
+.ql-snow .ql-fill,
+.ql-snow .ql-stroke.ql-fill {
+  fill: #444;
+}
+.ql-snow .ql-empty {
+  fill: none;
+}
+.ql-snow .ql-even {
+  fill-rule: evenodd;
+}
+.ql-snow .ql-thin,
+.ql-snow .ql-stroke.ql-thin {
+  stroke-width: 1;
+}
+.ql-snow .ql-transparent {
+  opacity: 0.4;
+}
+.ql-snow .ql-direction svg:last-child {
+  display: none;
+}
+.ql-snow .ql-direction.ql-active svg:last-child {
+  display: inline;
+}
+.ql-snow .ql-direction.ql-active svg:first-child {
+  display: none;
+}
+.ql-snow .ql-editor h1 {
+  font-size: 2em;
+}
+.ql-snow .ql-editor h2 {
+  font-size: 1.5em;
+}
+.ql-snow .ql-editor h3 {
+  font-size: 1.17em;
+}
+.ql-snow .ql-editor h4 {
+  font-size: 1em;
+}
+.ql-snow .ql-editor h5 {
+  font-size: 0.83em;
+}
+.ql-snow .ql-editor h6 {
+  font-size: 0.67em;
+}
+.ql-snow .ql-editor a {
+  text-decoration: underline;
+}
+.ql-snow .ql-editor blockquote {
+  border-left: 4px solid #ccc;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding-left: 16px;
+}
+.ql-snow .ql-editor code,
+.ql-snow .ql-editor pre {
+  background-color: #f0f0f0;
+  border-radius: 3px;
+}
+.ql-snow .ql-editor pre {
+  white-space: pre-wrap;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding: 5px 10px;
+}
+.ql-snow .ql-editor code {
+  font-size: 85%;
+  padding: 2px 4px;
+}
+.ql-snow .ql-editor pre.ql-syntax {
+  background-color: #23241f;
+  color: #f8f8f2;
+  overflow: visible;
+}
+.ql-snow .ql-editor img {
+  max-width: 100%;
+}
+.ql-snow .ql-picker {
+  color: #444;
+  display: inline-block;
+  float: left;
+  font-size: 14px;
+  font-weight: 500;
+  height: 24px;
+  position: relative;
+  vertical-align: middle;
+}
+.ql-snow .ql-picker-label {
+  cursor: pointer;
+  display: inline-block;
+  height: 100%;
+  padding-left: 8px;
+  padding-right: 2px;
+  position: relative;
+  width: 100%;
+}
+.ql-snow .ql-picker-label::before {
+  display: inline-block;
+  line-height: 22px;
+}
+.ql-snow .ql-picker-options {
+  background-color: #fff;
+  display: none;
+  min-width: 100%;
+  padding: 4px 8px;
+  position: absolute;
+  white-space: nowrap;
+}
+.ql-snow .ql-picker-options .ql-picker-item {
+  cursor: pointer;
+  display: block;
+  padding-bottom: 5px;
+  padding-top: 5px;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label {
+  color: #ccc;
+  z-index: 2;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
+  fill: #ccc;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
+  stroke: #ccc;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-options {
+  display: block;
+  margin-top: -1px;
+  top: 100%;
+  z-index: 1;
+}
+.ql-snow .ql-color-picker,
+.ql-snow .ql-icon-picker {
+  width: 28px;
+}
+.ql-snow .ql-color-picker .ql-picker-label,
+.ql-snow .ql-icon-picker .ql-picker-label {
+  padding: 2px 4px;
+}
+.ql-snow .ql-color-picker .ql-picker-label svg,
+.ql-snow .ql-icon-picker .ql-picker-label svg {
+  right: 4px;
+}
+.ql-snow .ql-icon-picker .ql-picker-options {
+  padding: 4px 0px;
+}
+.ql-snow .ql-icon-picker .ql-picker-item {
+  height: 24px;
+  width: 24px;
+  padding: 2px 4px;
+}
+.ql-snow .ql-color-picker .ql-picker-options {
+  padding: 3px 5px;
+  width: 152px;
+}
+.ql-snow .ql-color-picker .ql-picker-item {
+  border: 1px solid transparent;
+  float: left;
+  height: 16px;
+  margin: 2px;
+  padding: 0px;
+  width: 16px;
+}
+.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
+  position: absolute;
+  margin-top: -9px;
+  right: 0;
+  top: 50%;
+  width: 18px;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
+  content: attr(data-label);
+}
+.ql-snow .ql-picker.ql-header {
+  width: 98px;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  content: 'Heading 1';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  content: 'Heading 2';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  content: 'Heading 3';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  content: 'Heading 4';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  content: 'Heading 5';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  content: 'Heading 6';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  font-size: 2em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  font-size: 1.5em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  font-size: 1.17em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  font-size: 1em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  font-size: 0.83em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  font-size: 0.67em;
+}
+.ql-snow .ql-picker.ql-font {
+  width: 108px;
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
+  content: 'Sans Serif';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  content: 'Serif';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  content: 'Monospace';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-snow .ql-picker.ql-size {
+  width: 98px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  content: 'Small';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  content: 'Large';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  content: 'Huge';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  font-size: 10px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  font-size: 18px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  font-size: 32px;
+}
+.ql-snow .ql-color-picker.ql-background .ql-picker-item {
+  background-color: #fff;
+}
+.ql-snow .ql-color-picker.ql-color .ql-picker-item {
+  background-color: #000;
+}
+.ql-toolbar.ql-snow {
+  border: 1px solid #ccc;
+  box-sizing: border-box;
+  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+  padding: 8px;
+}
+.ql-toolbar.ql-snow .ql-formats {
+  margin-right: 15px;
+}
+.ql-toolbar.ql-snow .ql-picker-label {
+  border: 1px solid transparent;
+}
+.ql-toolbar.ql-snow .ql-picker-options {
+  border: 1px solid transparent;
+  box-shadow: rgba(0,0,0,0.2) 0 2px 8px;
+}
+.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label {
+  border-color: #ccc;
+}
+.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
+  border-color: #ccc;
+}
+.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,
+.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover {
+  border-color: #000;
+}
+.ql-toolbar.ql-snow + .ql-container.ql-snow {
+  border-top: 0px;
+}
+.ql-snow .ql-tooltip {
+  background-color: #fff;
+  border: 1px solid #ccc;
+  box-shadow: 0px 0px 5px #ddd;
+  color: #444;
+  padding: 5px 12px;
+  white-space: nowrap;
+}
+.ql-snow .ql-tooltip::before {
+  content: "Visit URL:";
+  line-height: 26px;
+  margin-right: 8px;
+}
+.ql-snow .ql-tooltip input[type=text] {
+  display: none;
+  border: 1px solid #ccc;
+  font-size: 13px;
+  height: 26px;
+  margin: 0px;
+  padding: 3px 5px;
+  width: 170px;
+}
+.ql-snow .ql-tooltip a.ql-preview {
+  display: inline-block;
+  max-width: 200px;
+  overflow-x: hidden;
+  text-overflow: ellipsis;
+  vertical-align: top;
+}
+.ql-snow .ql-tooltip a.ql-action::after {
+  border-right: 1px solid #ccc;
+  content: 'Edit';
+  margin-left: 16px;
+  padding-right: 8px;
+}
+.ql-snow .ql-tooltip a.ql-remove::before {
+  content: 'Remove';
+  margin-left: 8px;
+}
+.ql-snow .ql-tooltip a {
+  line-height: 26px;
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-preview,
+.ql-snow .ql-tooltip.ql-editing a.ql-remove {
+  display: none;
+}
+.ql-snow .ql-tooltip.ql-editing input[type=text] {
+  display: inline-block;
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+  border-right: 0px;
+  content: 'Save';
+  padding-right: 0px;
+}
+.ql-snow .ql-tooltip[data-mode=link]::before {
+  content: "Enter link:";
+}
+.ql-snow .ql-tooltip[data-mode=formula]::before {
+  content: "Enter formula:";
+}
+.ql-snow .ql-tooltip[data-mode=video]::before {
+  content: "Enter video:";
+}
+.ql-snow a {
+  color: #06c;
+}
+.ql-container.ql-snow {
+  border: 1px solid #ccc;
+}

+ 4 - 38
uni_modules/z-paging/changelog.md

@@ -1,38 +1,4 @@
-## 1.9.3(2021-07-12)
-1.延后首次加载自动请求的时机,使其在onLoad之后触发。  
-2.修复使用`safe-area-inset-bottom`时,距离顶部有段空白的BUG。  
-3.优化横向切换与下拉刷新手势的兼容,横向切换时禁止下拉刷新的细节调整。  
-## 1.9.2(2021-07-09)
-## 【注意】由V1.9.0起,fixed属性默认值为true,z-paging默认会铺满屏幕。老项目更新请注意,使用侧滑滚动切换选项卡或需要局部使用z-paging请设置:fixed="false"。如果您希望fixed属性默认为false,请参考文档:z-paging.com,将fixed默认值设置为false。 
-1.新增`completeByNoMore(data,nomore)`方法,支持在请求结束时自行控制是否有更多数据。  
-2.修复在微信小程序中,:fixed="false"时列表未展示的BUG。  
-3.进一步兼容和整合vue和nvue的写法。  
-4.其他细节调整与优化。
-## 1.9.1(2021-07-07)
-# 【注意】由V1.9.0起,fixed属性默认值为true,z-paging默认会铺满屏幕。老项目更新请注意,使用侧滑滚动切换选项卡或需要局部使用z-paging请设置:fixed="false"。如果您希望fixed属性默认为false,请参考文档:z-paging.com,将fixed默认值设置为false。
-1.修复在一些情况下空数据图无法展示或展示位置不正确的问题。  
-2.其他细节优化。
-## 1.9.0(2021-07-04)
-## 【注意1】由V1.9.0起,fixed属性默认值为true,z-paging默认会铺满屏幕。老项目更新请注意,使用侧滑滚动切换选项卡或需要局部使用z-paging请设置:fixed="false"。如果您希望fixed属性默认为false,请参考下方的【全局配置】,将fixed默认值设置为false。  
-1.修复使用slot="top"时可能出现的置顶view点击无效的问题。  
-2.修复设置`show-loading-more-when-reload`后,下拉刷新时底部加载更多view也处于loading状态的问题。  
-3.其他细节调整。
-
-## 1.8.9(2021-07-01)
-1.默认状态由`translateY(0px)`修改为`none`,修复因使用transform引发的子view fixed被降级为absoult的BUG。  
-2.修复在nvue安卓中,通过`loading-more-custom-style`修改加载更多view的高度时,加载更多view被裁剪的BUG。  
-3.去掉reload自动滚动到顶部的动画。  
-4.当`loading-more-enabled`设置为false,且`show-loading-more-when-reload`为true时,`show-loading-more-when-reload`优先。  
-5.修复在nvue 安卓中使用聊天记录模式,只有一页时底部有一段空白的BUG。  
-6.`concat`为false时,不再自动清空list。  
-## 1.8.8(2021-06-30)
-1.修复设置了`show-loading-more-when-reload`后,下拉刷新时展示加载更多loading且无数据时展示加载更多view的BUG。  
-2.修复在nvue中,在安卓设备上下拉刷新view底部有一根白色横线的BUG。  
-3.修复在nvue中,设置了`show-refresher-when-reload`后,部分平台reload时页面卡住闪退的BUG。  
-4.在nvue中支持`slot="top"`和`slot="bottom"`,使其写法与vue相同。  
-5.修改部分内部通用变量名,以避免在一些项目中引入mixins与其冲突的问题。  
-6.修复在nvue中,滚动到顶部会停顿一下,继续上拉才可以加载更多的BUG。  
-7.新增`concat`属性,支持控制是否自动拼接complete传过来的数组。
-## 1.8.7(2021-06-22)
-1.新增滑动切换选项卡简化写法及演示。  
-2.修复reload自动滚动到顶部无效的BUG。
+## 2.4.8(2022-11-03)
+1.修复在vue3+nvue中聊天记录模式颠倒的问题。  
+2.修复在vue3+nvue中`swiper-demo`列表无法展示的问题。  
+3.修复在vue3+vue中`swiper-demo`页面报错的问题。

+ 34 - 0
uni_modules/z-paging/components/z-paging-cell/z-paging-cell.vue

@@ -0,0 +1,34 @@
+<!-- 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]">
+		<slot />
+	</cell>
+	<!-- #endif -->
+	<!-- #ifndef APP-NVUE -->
+	<view :style="[cellStyle]">
+		<slot />
+	</view>
+	<!-- #endif -->
+</template>
+
+<script>
+	export default {
+		name: "z-paging-cell",
+		props: {
+			//cellStyle
+			cellStyle: {
+				type: Object,
+				default: function() {
+                    return {}
+                }
+			}
+		}
+	}
+</script>
+

+ 36 - 44
uni_modules/z-paging/components/z-paging-empty-view/z-paging-empty-view.vue

@@ -5,13 +5,12 @@
 
 <!-- 空数据占位view,此组件支持easycom规范,可以在项目中直接引用 -->
 <template>
-	<view class="zp-container" :style="[finalEmptyViewStyle]">
+	<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" :style="[emptyViewImgStyle]" :src="emptyImg"></image>
-			<image v-else class="zp-main-image" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg"></image>
+			<image v-if="!emptyViewImg.length" class="zp-main-image" :style="[emptyViewImgStyle]" :src="emptyImg" />
+			<image v-else class="zp-main-image" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg" />
 			<text class="zp-main-title" :style="[emptyViewTitleStyle]">{{emptyViewText}}</text>
-			<text v-if="showEmptyViewReload" class="zp-main-error-btn" :style="[emptyViewReloadStyle]"
-				@click="reloadClick">{{emptyViewReloadText}}</text>
+			<text v-if="showEmptyViewReload" class="zp-main-error-btn" :style="[emptyViewReloadStyle]" @click.stop="reloadClick">{{emptyViewReloadText}}</text>
 		</view>
 	</view>
 </template>
@@ -19,91 +18,80 @@
 <script>
 	import zStatic from '../z-paging/js/z-paging-static'
 	export default {
+		name: "z-paging-empty-view",
 		data() {
 			return {
-				base64Empty: zStatic.base64Empty,
-				base64Error: zStatic.base64Error
+				
 			};
 		},
 		props: {
 			//空数据描述文字
 			emptyViewText: {
 				type: String,
-				default: function() {
-					return '没有数据哦~'
-				}
+				default: '没有数据哦~'
 			},
 			//空数据图片
 			emptyViewImg: {
 				type: String,
-				default: function() {
-					return ''
-				}
+				default: ''
 			},
 			//是否显示空数据图重新加载按钮
 			showEmptyViewReload: {
 				type: Boolean,
-				default: function() {
-					return false
-				}
+				default: false
 			},
 			//空数据点击重新加载文字
 			emptyViewReloadText: {
 				type: String,
-				default: function() {
-					return '重新加载'
-				}
+				default: '重新加载'
 			},
 			//是否是加载失败
 			isLoadFailed: {
 				type: Boolean,
-				default: function() {
-					return false
-				}
+				default: false
 			},
 			//空数据图样式
 			emptyViewStyle: {
 				type: Object,
 				default: function() {
-					return {}
-				}
+                    return {}
+                }
 			},
 			//空数据图img样式
 			emptyViewImgStyle: {
 				type: Object,
 				default: function() {
-					return {}
+				    return {}
 				}
 			},
 			//空数据图描述文字样式
 			emptyViewTitleStyle: {
 				type: Object,
 				default: function() {
-					return {}
+				    return {}
 				}
 			},
 			//空数据图重新加载按钮样式
 			emptyViewReloadStyle: {
 				type: Object,
 				default: function() {
-					return {}
+				    return {}
 				}
 			},
 			//空数据图z-index
 			emptyViewZIndex: {
 				type: Number,
-				default: function() {
-					return 9
-				}
+				default: 9
+			},
+			//空数据图片是否使用fixed布局并铺满z-paging
+			emptyViewFixed: {
+				type: Boolean,
+				default: true
 			}
 		},
 		computed: {
 			emptyImg() {
-				if (this.isLoadFailed) {
-					return this.base64Error;
-				} else {
-					return this.base64Empty;
-				}
+                return this.isLoadFailed ? zStatic.base64Error : zStatic.base64Empty;
 			},
 			finalEmptyViewStyle(){
 				this.emptyViewStyle['z-index'] = this.emptyViewZIndex;
@@ -113,38 +101,42 @@
 		methods: {
 			reloadClick() {
 				this.$emit('reload');
+			},
+			emptyViewClick() {
+				this.$emit('viewClick');
 			}
 		}
 	}
 </script>
 
 <style scoped>
-	.zp-container {
+	.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%;
-		display: flex;
 		/* #endif */
 		/* #ifdef APP-NVUE */
 		flex: 1;
 		/* #endif */
-		align-items: center;
-		justify-content: center;
 	}
 
-	.zp-main {
+	.zp-main{
 		/* #ifndef APP-NVUE */
 		display: flex;
-		margin-top: -150rpx;
-		/* #endif */
-		/* #ifdef APP-NVUE */
-		margin-top: -100rpx;
 		/* #endif */
 		flex-direction: column;
 		align-items: center;
+        padding: 50rpx 0rpx;
 	}
 
 	.zp-main-image {

+ 68 - 11
uni_modules/z-paging/components/z-paging-swiper-item/z-paging-swiper-item.vue

@@ -6,9 +6,20 @@
 <!-- 滑动切换选项卡swiper-item,此组件支持easycom规范,可以在项目中直接引用 -->
 <template>
 	<view class="zp-swiper-item-container">
-		<z-paging ref="paging" :fixed="false" @query="_queryList" @listChange="_updateList" :mounted-auto-call-reload="false"
-			style="height: 100%;">
-			<slot></slot>
+		<z-paging ref="paging" :fixed="false" 
+			:useVirtualList="useVirtualList" :useInnerList="useInnerList" :cellKeyName="cellKeyName" :innerListStyle="innerListStyle" 
+			:preloadPage="preloadPage" :cellHeightMode="cellHeightMode" :virtualScrollFps="virtualScrollFps" :virtualListCol="virtualListCol"
+			@query="_queryList" @listChange="_updateList" :mounted-auto-call-reload="false" style="height: 100%;">
+			<slot />
+			<template v-slot:header>
+				<slot name="header"/>
+			</template>
+			<template v-slot:cell="{item,index}">
+				<slot name="cell" :item="item" :index="index"/>
+			</template>
+			<template v-slot:footer>
+				<slot name="footer"/>
+			</template>
 		</z-paging>
 	</view>
 </template>
@@ -17,9 +28,7 @@
 	import zPaging from '../z-paging/z-paging'
 	export default {
 		name: "z-paging-swiper-item",
-		components: {
-			zPaging
-		},
+		components: { zPaging },
 		data() {
 			return {
 				firstLoaded: false
@@ -40,6 +49,48 @@
 					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 {};
+				}
+			},
+			//预加载的列表可视范围(列表高度)页数,默认为7,即预加载当前页及上下各7页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
+			preloadPage: {
+				type: [Number, String],
+				default: 7
+			},
+			//虚拟列表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: {
@@ -47,9 +98,15 @@
 					if (newVal === this.tabIndex) {
 						//懒加载,当滑动到当前的item时,才去加载
 						if (!this.firstLoaded) {
-							setTimeout(() => {
-								this.$refs.paging.reload();
-							}, 5);
+							this.$nextTick(()=>{
+								let delay = 5;
+								// #ifdef MP-TOUTIAO
+								delay = 100;
+								// #endif
+								setTimeout(() => {
+									this.$refs.paging.reload();
+								}, delay);
+							})
 						}
 					}
 				},
@@ -64,8 +121,8 @@
 				this.firstLoaded = true;
 				this.$refs.paging.complete(data);
 			},
-			_queryList(pageNo, pageSize) {
-				this.$emit('query', pageNo, pageSize);
+			_queryList(pageNo, pageSize, from) {
+				this.$emit('query', pageNo, pageSize, from);
 			},
 			_updateList(list) {
 				this.$emit('updateList', list);

+ 135 - 28
uni_modules/z-paging/components/z-paging-swiper/z-paging-swiper.vue

@@ -5,14 +5,23 @@
 
 <!-- 滑动切换选项卡swiper,此组件支持easycom规范,可以在项目中直接引用 -->
 <template>
-	<view :class="fixed?'zp-swiper-container zp-swiper-container-fixed':'zp-swiper-container'" :style="[swiperStyle]">
-		<slot v-if="$slots.top" name="top"></slot>
+	<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="$slots.top" name="top" />
 		<view class="zp-swiper-super">
-			<view class="zp-swiper">
-				<slot/></slot>
+			<view v-if="$slots.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="$slots.right" :class="{'zp-swiper-right':true,'zp-absoulte zp-right':isOldWebView}">
+				<slot name="right" />
 			</view>
 		</view>
-		<slot v-if="$slots.bottom" name="bottom"></slot>
+		<slot v-if="$slots.bottom" name="bottom" />
 	</view>
 </template>
 
@@ -21,7 +30,9 @@
 		name: "z-paging-swiper",
 		data() {
 			return {
-				systemInfo: null
+				systemInfo: null,
+				cssSafeAreaInsetBottom: -1,
+				swiperContentStyle: {}
 			};
 		},
 		props: {
@@ -34,45 +45,117 @@
 			safeAreaInsetBottom: {
 				type: Boolean,
 				default: false
+			},
+			//z-paging-swiper样式
+			swiperStyle: {
+				type: Object,
+				default: function() {
+					return {};
+				},
 			}
 		},
 		mounted() {
 			this.$nextTick(() => {
 				this.systemInfo = uni.getSystemInfoSync();
 			})
+			// #ifndef APP-PLUS
+			this._getCssSafeAreaInsetBottom();
+			// #endif
+			this._updateLeftAndRightWidth();
+
+			this.swiperContentStyle = {'flex': '1'};
+			// #ifndef APP-NVUE
+			this.swiperContentStyle = {width: '100%',height: '100%'};
+			// #endif
 		},
 		computed: {
-			swiperStyle() {
-				if (!this.systemInfo) {
-					return {};
-				}
-				let swiperStyle = {};
-				const windowTop = this.systemInfo.windowTop;
+			finalSwiperStyle() {
+				const swiperStyle = this.swiperStyle;
+				if (!this.systemInfo) return swiperStyle;
+				let windowTop = this.systemInfo.windowTop;
+				//暂时修复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) windowTop = 0;
+				// #endif
 				const windowBottom = this.systemInfo.windowBottom;
 				if (this.fixed) {
-					if (windowTop && windowTop !== undefined) {
+					if (windowTop && !swiperStyle.top) {
 						swiperStyle.top = windowTop + 'px';
 					}
-					let bottom = 0;
-					if (windowBottom && windowBottom !== undefined) {
-						bottom = windowBottom;
-					}
-					if (this.safeAreaInsetBottom) {
-						bottom += this.safeAreaBottom;
+					if (!swiperStyle.bottom) {
+						let bottom = windowBottom ? windowBottom : 0;
+						if (this.safeAreaInsetBottom) {
+							bottom += this.safeAreaBottom;
+						}
+						if(bottom > 0){
+							swiperStyle.bottom = bottom + 'px';
+						}
 					}
-					swiperStyle.bottom = bottom + 'px';
 				}
 				return swiperStyle;
 			},
 			safeAreaBottom() {
-				if (!this.systemInfo) {
-					return 0;
-				}
+				if (!this.systemInfo) return 0;
 				let safeAreaBottom = 0;
-				// #ifdef APP-PLUS || H5 || MP-WEIXIN
+				// #ifdef APP-PLUS
 				safeAreaBottom = this.systemInfo.safeAreaInsets.bottom || 0;
 				// #endif
+				// #ifndef APP-PLUS
+				safeAreaBottom = this.cssSafeAreaInsetBottom === -1 ? 0 : this.cssSafeAreaInsetBottom;
+				// #endif
 				return safeAreaBottom;
+			},
+			isOldWebView() {
+				// #ifndef APP-NVUE || MP-KUAISHOU
+				try {
+					const systemInfos = uni.getSystemInfoSync().system.split(' ');
+					const deviceType = systemInfos[0];
+					const version = parseInt(systemInfos[1].slice(0,1));
+					if ((deviceType === 'iOS' && version <= 10) || (deviceType === 'Android' && version <= 6)) {
+						return true;
+					}
+				} catch(e){
+					return false;
+				}
+				// #endif
+				return false;
+			}
+		},
+		methods: {
+			//更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
+			updateLeftAndRightWidth() {
+				this.$nextTick(() => {
+					this._updateLeftAndRightWidth();
+				})
+			},
+			//通过获取css设置的底部安全区域占位view高度设置bottom距离
+			_getCssSafeAreaInsetBottom() {
+				const query = uni.createSelectorQuery().in(this);
+				query.select('.zp-safe-area-inset-bottom').boundingClientRect(res => {
+					if (res) {
+						this.cssSafeAreaInsetBottom = res.height;
+					}
+				}).exec();
+			},
+			//获取slot="left"和slot="right"宽度并且更新布局
+			_updateLeftAndRightWidth() {
+				if (!this.isOldWebView) return;
+				this.$nextTick(() => {
+					let delayTime = 0;
+					// #ifdef MP-BAIDU
+					delayTime = 10;
+					// #endif
+					setTimeout(() => {
+						['left','right'].map(position => {
+							const query = uni.createSelectorQuery().in(this);
+							query.select(`.zp-swiper-${position}`).boundingClientRect(res => {
+								this.$set(this.swiperContentStyle, position, res ? res.width + 'px' : '0px');
+							}).exec();
+						})
+					}, delayTime)
+				})
 			}
 		}
 	}
@@ -98,25 +181,49 @@
 		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;
 		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;
-		left: 0;
-		/* #endif */
-		/* #ifdef APP-NVUE */
-		flex: 1;
+		width: auto;
 		/* #endif */
 	}
 	
+	.zp-right{
+		right: 0;
+	}
+	
 	.zp-swiper-item {
 		height: 100%;
 	}

+ 68 - 85
uni_modules/z-paging/components/z-paging/components/z-paging-load-more.vue

@@ -1,69 +1,70 @@
-<!-- z-paging -->
-<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
-<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
-<!-- 反馈QQ群:790460711 -->
-
-<!-- 上拉加载更多view -->
+<!-- [z-paging]上拉加载更多view -->
 <template>
-	<view class="zp-load-more-container" :style="[zConfig.loadingMoreCustomStyle]">
-		<text
-			:class="zConfig.defaultThemeStyle==='white'?'zp-loading-more-line zp-loading-more-line-white':'zp-loading-more-line zp-loading-more-line-black'"
-			:style="[zConfig.loadingMoreNoMoreLineCustomStyle]"
-			v-if="zConfig.showLoadingMoreNoMoreLine&&zConfig.loadingStatus===2"></text>
-		<!-- #ifndef APP-NVUE -->
-		<image v-if="zConfig.loadingStatus===1&&zConfig.loadingMoreLoadingIconCustomImage.length"
-			:src="zConfig.loadingMoreLoadingIconCustomImage" class="zp-loading-more-line-loading-custom-image">
-		</image>
-		<image
-			v-if="zConfig.loadingStatus===1&&zConfig.loadingMoreLoadingIconType==='flower'&&!zConfig.loadingMoreLoadingIconCustomImage.length"
-			class="zp-loading-more-line-loading-image" :style="[zConfig.loadingMoreLoadingIconCustomStyle]"
-			:src="zConfig.defaultThemeStyle==='white'?base64FlowerWhite:base64Flower">
-		</image>
-		<!-- #endif -->
-		<!-- #ifdef APP-NVUE -->
-		<view>
-			<loading-indicator v-if="zConfig.loadingStatus===1"
-				:style="[{color:zConfig.defaultThemeStyle==='white'?'white':'#777777'}]" :animating="true"
-				class="zp-loading-more-line-loading-image">
-			</loading-indicator>
-		</view>
-		<!-- #endif -->
-		<text
-			v-if="zConfig.loadingStatus===1&&zConfig.loadingMoreLoadingIconType==='circle'&&!zConfig.loadingMoreLoadingIconCustomImage.length"
-			:class="zConfig.defaultThemeStyle==='white'?'zp-loading-more-line-loading-view zp-loading-more-line-loading-view-white':'zp-loading-more-line-loading-view zp-loading-more-line-loading-view-black'"
-			:style="[zConfig.loadingMoreLoadingIconCustomStyle]"></text>
-		<text
-			:class="zConfig.defaultThemeStyle==='white'?'zp-loading-more-text zp-loading-more-text-white':'zp-loading-more-text zp-loading-more-text-black'">{{ownLoadingMoreText}}</text>
-		<text
-			:class="zConfig.defaultThemeStyle==='white'?'zp-loading-more-line zp-loading-more-line-white':'zp-loading-more-line zp-loading-more-line-black'"
-			:style="[zConfig.loadingMoreNoMoreLineCustomStyle]"
-			v-if="zConfig.showLoadingMoreNoMoreLine&&zConfig.loadingStatus===2"></text>
+	<view class="zp-l-container" :style="[c.customStyle]" @click="doClick">
+		<template v-if="!c.hideContent">
+			<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" class="zp-l-line" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
+			<!-- #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}" />
+			<image v-if="finalStatus===M.Loading&&finalLoadingIconType==='flower'&&!c.loadingIconCustomImage.length"
+				class="zp-line-loading-image" :style="[c.iconCustomStyle]" :src="zTheme.flower[ts]" />
+			<!-- #endif -->
+			<!-- #ifdef APP-NVUE -->
+			<view>
+				<loading-indicator v-if="finalStatus===M.Loading&&finalLoadingIconType!=='circle'" class="zp-line-loading-image" :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" :style="[{borderColor:zTheme.circleBorder[ts],borderTopColor:zTheme.circleBorderTop[ts]},c.iconCustomStyle]" />
+			<text class="zp-l-text" :style="[{color:zTheme.title[ts]},c.titleCustomStyle]">{{ownLoadingMoreText}}</text>
+			<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" class="zp-l-line" :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 {
-				base64Arrow: zStatic.base64Arrow,
-				base64Flower: zStatic.base64Flower,
-				base64FlowerWhite: zStatic.base64FlowerWhite,
+				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'],
+		props: ['config'],
 		computed: {
+			ts() {
+				return this.c.defaultThemeStyle;
+			},
+			c() {
+				return this.config;
+			},
 			ownLoadingMoreText() {
-				const loadingMoreText = this.loadingStatusTextMap[this.zConfig.loadingStatus];
-				return loadingMoreText;
+				const statusTextArr = [this.c.defaultText,this.c.loadingText,this.c.noMoreText,this.c.failText];
+				return statusTextArr[this.finalStatus];
 			},
-			loadingStatusTextMap() {
-				return {
-					0: this.zConfig.loadingMoreDefaultText,
-					1: this.zConfig.loadingMoreLoadingText,
-					2: this.zConfig.loadingMoreNoMoreText,
-					3: this.zConfig.loadingMoreFailText,
-				}
+			finalStatus() {
+				if (this.c.defaultAsLoading && this.c.status === this.M.Default) return this.M.Loading;
+				return this.c.status;
+			},
+			finalLoadingIconType() {
+				// #ifdef APP-NVUE
+				return 'flower';
+				// #endif
+				return this.c.loadingIconType;
+			}
+		},
+		methods: {
+			doClick() {
+				this.$emit('doClick');
 			}
 		}
 	}
@@ -72,7 +73,7 @@
 <style scoped>
 	@import "../css/z-paging-static.css";
 
-	.zp-load-more-container {
+	.zp-l-container {
 		height: 80rpx;
 		font-size: 27rpx;
 		/* #ifndef APP-NVUE */
@@ -84,75 +85,57 @@
 		justify-content: center;
 	}
 
-	.zp-loading-more-line-loading-custom-image {
+	.zp-l-line-loading-custom-image {
 		color: #a4a4a4;
 		margin-right: 8rpx;
 		width: 28rpx;
 		height: 28rpx;
+	}
+	
+	.zp-l-line-loading-custom-image-animated{
 		/* #ifndef APP-NVUE */
 		animation: loading-circle 1s linear infinite;
 		/* #endif */
 	}
 
-	.zp-loading-more-line-loading-view {
+	.zp-l-circle-loading-view {
 		margin-right: 8rpx;
-		width: 22rpx;
+		width: 23rpx;
 		height: 23rpx;
 		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-loading-more-line-loading-view-black {
-		border-color: #c8c8c8;
-		border-top-color: #444444;
-	}
-
-	.zp-loading-more-line-loading-view-white {
-		border-color: #aaaaaa;
-		border-top-color: #ffffff;
-	}
-
-	.zp-loading-more-text {
+	.zp-l-text {
 		/* #ifdef APP-NVUE */
 		font-size: 30rpx;
 		margin: 0rpx 10rpx;
 		/* #endif */
 	}
 
-	.zp-loading-more-text-black {
-		color: #a4a4a4;
-	}
-
-	.zp-loading-more-text-white {
-		color: #efefef;
-	}
-
-	.zp-loading-more-line {
+	.zp-l-line {
 		height: 1px;
 		width: 100rpx;
 		margin: 0rpx 10rpx;
 	}
 
-	.zp-loading-more-line-black {
-		background-color: #eeeeee;
-	}
-
-	.zp-loading-more-line-white {
-		background-color: #efefef;
-	}
-
+	/* #ifndef APP-NVUE */
 	@keyframes loading-circle {
 		0% {
 			-webkit-transform: rotate(0deg);
 			transform: rotate(0deg);
 		}
-
 		100% {
 			-webkit-transform: rotate(360deg);
 			transform: rotate(360deg);
 		}
 	}
+	/* #endif */
 </style>

+ 89 - 166
uni_modules/z-paging/components/z-paging/components/z-paging-refresh.vue

@@ -1,133 +1,105 @@
-<!-- z-paging -->
-<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
-<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
-<!-- 反馈QQ群:790460711 -->
-
-<!-- 下拉刷新view -->
+<!-- [z-paging]下拉刷新view -->
 <template>
 	<view style="height: 100%;">
-		<view
-			:class="['zp-custom-refresher-container',{'zp-custom-refresher-container-padding':showRefresherUpdateTime}]"
-			style="height: 100%;">
-			<view class="zp-custom-refresher-left">
-				<image v-if="refresherStatus!==2" :class="refresherLeftImageClass"
-					:style="[{width: showRefresherUpdateTime?'36rpx':'30rpx',height: showRefresherUpdateTime?'36rpx':'30rpx','margin-right': showRefresherUpdateTime?'20rpx':'8rpx'}]"
-					:src="defaultThemeStyle==='white'?base64ArrowWhite:base64Arrow">
-				</image>
+		<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-loading-more-line-loading-image zp-custom-refresher-left-image"
-					:style="[{width: showRefresherUpdateTime?'36rpx':'30rpx',height: showRefresherUpdateTime?'36rpx':'30rpx','margin-right': showRefresherUpdateTime?'20rpx':'8rpx'}]"
-					:src="defaultThemeStyle==='white'?base64FlowerWhite:base64Flower">
-				</image>
+				<image v-else class="zp-line-loading-image zp-r-left-image" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
 				<!-- #endif -->
 				<!-- #ifdef APP-NVUE -->
-				<view v-else :style="[{'margin-right':showRefresherUpdateTime?'18rpx':'12rpx'}]">
-					<loading-indicator
-						:class="systemInfo.platform==='ios'?'zp-loading-image-ios':'zp-loading-image-android'"
-						:style="[{color:defaultThemeStyle==='white'?'white':'#777777'}]" :animating="true">
-					</loading-indicator>
+				<view v-else :style="[{'margin-right':showUpdateTime?'18rpx':'12rpx'}]">
+					<loading-indicator :class="isIos?'zp-loading-image-ios':'zp-loading-image-android'" 
+					:style="[{color:zTheme.indicator[ts]},imgStyle]" :animating="true" />
 				</view>
 				<!-- #endif -->
 			</view>
-			<view class="zp-custom-refresher-right">
-				<text class="zp-custom-refresher-right-text"
-					:style="[refresherRightTextStyle]">{{refresherStatusTextMap[refresherStatus]||refresherDefaultText}}
-				</text>
-				<text class="zp-custom-refresher-right-text zp-custom-refresher-right-time-text"
-					:style="[refresherRightTextStyle]"
-					v-if="showRefresherUpdateTime&&refresherTimeText.length">{{refresherTimeText}}
+			<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 zp-r-right-time-text" :style="[rightTextStyle,updateTimeStyle]">
+					{{refresherTimeText}}
 				</text>
 			</view>
 		</view>
 	</view>
 </template>
 <script>
-	const systemInfo = uni.getSystemInfoSync();
 	import zStatic from '../js/z-paging-static'
-	import {
-		getRefesrherFormatTimeByKey
-	} from '../js/z-paging-utils'
+	import u from '../js/z-paging-utils'
+	import Enum from '../js/z-paging-enum'
+	
 	export default {
 		name: 'z-paging-refresh',
 		data() {
 			return {
-				systemInfo: systemInfo,
-				base64Arrow: zStatic.base64Arrow,
-				base64ArrowWhite: zStatic.base64ArrowWhite,
-				base64Flower: zStatic.base64Flower,
-				base64FlowerWhite: zStatic.base64FlowerWhite,
+				R: Enum.Refresher,
+				isIos: uni.getSystemInfoSync().platform === 'ios',
 				refresherTimeText: '',
-				isRefresherLeftImageClassLoaded: false
+				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: {
-			'refresherStatus': {
-				default: 0
+		props: ['status', 'defaultThemeStyle', 'defaultText', 'pullingText', 'refreshingText', 'completeText', 'defaultImg', 'pullingImg', 
+			'refreshingImg', 'completeImg', 'showUpdateTime', 'updateTimeKey', 'imgStyle', 'titleStyle', 'updateTimeStyle', 'updateTimeTextMap'
+		],
+		computed: {
+			ts() {
+				return this.defaultThemeStyle;
 			},
-			'defaultThemeStyle': {},
-			'refresherDefaultText': {},
-			'refresherPullingText': {},
-			'refresherPullingText': {},
-			'refresherRefreshingText': {},
-			'showRefresherUpdateTime': {
-				default: false
+			statusTextArr() {
+				this.updateTime();
+				return [this.defaultText,this.pullingText,this.refreshingText,this.completeText];
 			},
-			'refresherUpdateTimeKey': {}
-		},
-		computed: {
-			refresherStatusTextMap() {
-				this.updateTime(this.refresherUpdateTimeKey);
-				return {
-					0: this.refresherDefaultText,
-					1: this.refresherPullingText,
-					2: this.refresherRefreshingText
-				};
+			currentTitle() {
+				return this.statusTextArr[this.status] || this.defaultText;
 			},
-			refresherLeftImageClass() {
-				let refresherLeftImageClass = '';
-				if (this.refresherStatus === 0) {
-					if (this.isRefresherLeftImageClassLoaded) {
-						refresherLeftImageClass = 'zp-custom-refresher-left-image zp-custom-refresher-arrow-down';
-					} else {
-						this.isRefresherLeftImageClassLoaded = true;
-						refresherLeftImageClass =
-							'zp-custom-refresher-left-image zp-custom-refresher-arrow-down-no-duration';
-					}
-				} else {
-					refresherLeftImageClass = 'zp-custom-refresher-left-image zp-custom-refresher-arrow-top';
-				}
-				return refresherLeftImageClass;
+			leftImageClass() {
+				if (this.status === this.R.Complete) return 'zp-r-left-image-pre-size';
+				return `zp-r-left-image zp-r-left-image-pre-size ${this.status === this.R.Default ? 'zp-r-arrow-down' : 'zp-r-arrow-top'}`;
+			},
+			leftImageStyle() {
+				const showUpdateTime = this.showUpdateTime;
+				const size = showUpdateTime ? '36rpx' : '30rpx';
+				return {width: size,height: size,'margin-right': showUpdateTime ? '20rpx' : '9rpx'};
 			},
-			refresherRightTextStyle() {
-				let refresherRightTextStyle = {};
-				let color = '#555555';
-				if (this.defaultThemeStyle === 'white') {
-					color = '#efefef';
+			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];;
 				}
+				return '';
+			},
+			rightTextStyle() {
+				let stl = {};
 				// #ifdef APP-NVUE
-				if (this.showRefresherUpdateTime) {
-					refresherRightTextStyle = {
-						'height': '40rpx',
-						'line-height': '40rpx'
-					};
-				} else {
-					refresherRightTextStyle = {
-						'height': '80rpx',
-						'line-height': '80rpx'
-					};
-				}
+				const textHeight = this.showUpdateTime ? '40rpx' : '80rpx';
+				stl = {'height': textHeight, 'line-height': textHeight}
 				// #endif
-				refresherRightTextStyle['color'] = color;
-				return refresherRightTextStyle;
+				stl['color'] = this.zTheme.title[this.ts];
+				return stl;
 			}
 		},
 		methods: {
-			updateTime(refresherUpdateTimeKey) {
-				if (!refresherUpdateTimeKey) {
-					refresherUpdateTimeKey = this.refresherUpdateTimeKey;
-				}
-				if (this.showRefresherUpdateTime) {
-					this.refresherTimeText = getRefesrherFormatTimeByKey(refresherUpdateTimeKey);
+			updateTime() {
+				if (this.showUpdateTime) {
+					this.refresherTimeText = u.getRefesrherFormatTimeByKey(this.updateTimeKey, this.updateTimeTextMap);
 				}
 			}
 		}
@@ -137,82 +109,57 @@
 <style scoped>
 	@import "../css/z-paging-static.css";
 
-	.zp-custom-refresher-container {
+	.zp-r-container {
 		/* #ifndef APP-NVUE */
 		display: flex;
+		height: 100%;
 		/* #endif */
 		flex-direction: row;
 		justify-content: center;
 		align-items: center;
 	}
 
-	.zp-custom-refresher-container-padding {
+	.zp-r-container-padding {
 		/* #ifdef APP-NVUE */
 		padding: 15rpx 0rpx;
 		/* #endif */
 	}
 
-	.zp-custom-refresher-left {
+	.zp-r-left {
 		/* #ifndef APP-NVUE */
 		display: flex;
 		/* #endif */
 		flex-direction: row;
 		align-items: center;
 		overflow: hidden;
-	}
-
-	.zp-custom-refresher-left-image {
-		/* #ifndef APP-NVUE */
-		transform: rotate(180deg);
-		margin-top: 2rpx;
-		/* #endif */
 		/* #ifdef MP-ALIPAY */
-		margin-top: 0rpx;
+		margin-top: -4rpx;
 		/* #endif */
-		/* #ifdef APP-NVUE */
+	}
+
+	.zp-r-left-image {
 		transition-duration: .2s;
 		transition-property: transform;
 		color: #666666;
-		/* #endif */
 	}
-
-	.zp-custom-refresher-arrow-top {
+	
+	.zp-r-left-image-pre-size{
 		/* #ifndef APP-NVUE */
-		animation: refresher-arrow-top .2s linear;
-		-webkit-animation: refresher-arrow-top .2s linear;
-		animation-fill-mode: forwards;
-		-webkit-animation-fill-mode: forwards;
-		/* #endif */
-		/* #ifdef APP-NVUE */
-		transform: rotate(0deg);
+		width: 30rpx;
+		height: 30rpx;
+		overflow: hidden;
 		/* #endif */
 	}
 
-	.zp-custom-refresher-arrow-down {
-		/* #ifndef APP-NVUE */
-		animation: refresher-arrow-down .2s linear;
-		-webkit-animation: refresher-arrow-down .2s linear;
-		animation-fill-mode: forwards;
-		-webkit-animation-fill-mode: forwards;
-		/* #endif */
-		/* #ifdef APP-NVUE */
-		transform: rotate(180deg);
-		/* #endif */
+	.zp-r-arrow-top {
+		transform: rotate(0deg);
 	}
 
-	.zp-custom-refresher-arrow-down-no-duration {
-		/* #ifndef APP-NVUE */
-		animation: refresher-arrow-down 0s linear;
-		-webkit-animation: refresher-arrow-down 0s linear;
-		animation-fill-mode: forwards;
-		-webkit-animation-fill-mode: forwards;
-		/* #endif */
-		/* #ifdef APP-NVUE */
+	.zp-r-arrow-down {
 		transform: rotate(180deg);
-		/* #endif */
 	}
 
-	.zp-custom-refresher-right {
+	.zp-r-right {
 		font-size: 27rpx;
 		/* #ifndef APP-NVUE */
 		display: flex;
@@ -222,38 +169,14 @@
 		justify-content: center;
 	}
 
-	.zp-custom-refresher-right-text {
+	.zp-r-right-text {
 		/* #ifdef APP-NVUE */
 		font-size: 28rpx;
 		/* #endif */
 	}
 
-	.zp-custom-refresher-right-time-text {
+	.zp-r-right-time-text {
 		margin-top: 10rpx;
 		font-size: 24rpx;
 	}
-
-	@keyframes refresher-arrow-top {
-		0% {
-			-webkit-transform: rotate(180deg);
-			transform: rotate(180deg);
-		}
-
-		100% {
-			-webkit-transform: rotate(0deg);
-			transform: rotate(0deg);
-		}
-	}
-
-	@keyframes refresher-arrow-down {
-		0% {
-			-webkit-transform: rotate(0deg);
-			transform: rotate(0deg);
-		}
-
-		100% {
-			-webkit-transform: rotate(180deg);
-			transform: rotate(180deg);
-		}
-	}
 </style>

+ 3 - 0
uni_modules/z-paging/components/z-paging/config/index.js

@@ -0,0 +1,3 @@
+// z-paging全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
+
+export default {}

+ 80 - 10
uni_modules/z-paging/components/z-paging/css/z-paging-main.css

@@ -1,8 +1,4 @@
-/* z-paging
-github地址:https://github.com/SmileZXLee/uni-z-paging
-dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-反馈QQ群:790460711
-*/
+/* [z-paging]公共css*/
 
 .z-paging-content {
 	position: relative;
@@ -10,11 +6,12 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	display: flex;
 	width: 100%;
 	height: 100%;
+	overflow: hidden;
 	/* #endif */
 	flex-direction: column;
 }
 
-.z-paging-content-fixed {
+.z-paging-content-fixed, .zp-loading-fixed {
 	position: fixed;
 	/* #ifndef APP-NVUE */
 	height: auto;
@@ -26,8 +23,7 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	right: 0;
 }
 
-.zp-page-scroll-top,
-.zp-page-scroll-bottom {
+.zp-page-top,.zp-page-bottom {
 	/* #ifndef APP-NVUE */
 	width: auto;
 	/* #endif */
@@ -37,14 +33,46 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	z-index: 999;
 }
 
+.zp-page-left,.zp-page-right{
+	/* #ifndef APP-NVUE */
+	height: 100%;
+	/* #endif */
+}
+
 .zp-scroll-view-super {
 	flex: 1;
 	position: relative;
 }
 
-.zp-scroll-view {
+.zp-view-super{
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+}
+
+.zp-custom-refresher-container {
+	overflow: hidden;
+}
+
+.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-right{
+	right: 0;
 }
 
 .zp-scroll-view-absolute {
@@ -53,6 +81,16 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	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%;
@@ -78,6 +116,10 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 .zp-paging-container {
 	flex: 1;
 	position: relative;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
 }
 
 .zp-chat-record-loading-container {
@@ -141,13 +183,33 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	z-index: 999;
 }
 
-.zp-empty-view{
+.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;
@@ -155,3 +217,11 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	justify-content: center;
 	width: 750rpx;
 }
+
+.zp-n-list-container{
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+	flex: 1;
+}

+ 5 - 8
uni_modules/z-paging/components/z-paging/css/z-paging-static.css

@@ -1,11 +1,6 @@
-/* z-paging
-github地址:https://github.com/SmileZXLee/uni-z-paging
-dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-反馈QQ群:790460711
+/* [z-paging]公用的静态css资源 */
 
-公用的静态css资源 */
-
-.zp-loading-more-line-loading-image {
+.zp-line-loading-image {
 	margin-right: 8rpx;
 	width: 28rpx;
 	height: 28rpx;
@@ -25,14 +20,16 @@ dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
 	height: 32rpx;
 }
 
+/* #ifndef APP-NVUE */
 @keyframes loading-flower {
 	0% {
 		-webkit-transform: rotate(0deg);
 		transform: rotate(0deg);
 	}
-
 	to {
 		-webkit-transform: rotate(1turn);
 		transform: rotate(1turn);
 	}
 }
+/* #endif */
+

+ 22 - 0
uni_modules/z-paging/components/z-paging/i18n/en.json

@@ -0,0 +1,22 @@
+{	
+	"zp.refresher.default": "Pull down to refresh",
+	"zp.refresher.pulling": "Release to refresh",
+	"zp.refresher.refreshing": "Refreshing...",
+	"zp.refresher.complete": "Refresh succeeded",
+	
+	"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..."
+}

+ 8 - 0
uni_modules/z-paging/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
+}

+ 22 - 0
uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json

@@ -0,0 +1,22 @@
+{	
+	"zp.refresher.default": "继续下拉刷新",
+	"zp.refresher.pulling": "松开立即刷新",
+	"zp.refresher.refreshing": "正在刷新...",
+	"zp.refresher.complete": "刷新成功",
+	
+	"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": "加载中..."
+}

+ 22 - 0
uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json

@@ -0,0 +1,22 @@
+{	
+	"zp.refresher.default": "繼續下拉重繪",
+	"zp.refresher.pulling": "鬆開立即重繪",
+	"zp.refresher.refreshing": "正在重繪...",
+	"zp.refresher.complete": "重繪成功",
+	
+	"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": "加載中..."
+}

+ 100 - 0
uni_modules/z-paging/components/z-paging/js/modules/back-to-top.js

@@ -0,0 +1,100 @@
+// [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: function() {
+				return u.gc('backToTopStyle', {});
+			},
+		},
+		//iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向,默认为是
+		enableBackToTop: {
+			type: Boolean,
+			default: u.gc('enableBackToTop', true)
+		},
+	},
+	data() {
+		return {
+			backToTopClass: 'zp-back-to-top zp-back-to-top-hide',
+			lastBackToTopShowTime: 0,
+			showBackToTopClass: false,
+		}
+	},
+	computed: {
+		finalEnableBackToTop() {
+			return this.usePageScroll ? false : this.enableBackToTop;
+		},
+		finalBackToTopThreshold() {
+			return u.convertTextToPx(this.backToTopThreshold);
+		},
+		finalBackToTopStyle() {
+			const backToTopStyle = this.backToTopStyle;
+			if (!backToTopStyle.bottom) {
+				backToTopStyle.bottom = this.windowBottom + u.convertTextToPx(this.backToTopBottom) + 'px';
+			}
+			if(!backToTopStyle.position){
+				backToTopStyle.position = this.usePageScroll ? 'fixed': 'absolute';
+			}
+			return backToTopStyle;
+		},
+	},
+	methods: {
+		//点击返回顶部
+		_backToTopClick() {
+			!this.backToTopWithAnimate && this._checkShouldShowBackToTop(0);
+			this.scrollToTop(this.backToTopWithAnimate);
+		},
+		//判断是否要显示返回顶部按钮
+		_checkShouldShowBackToTop(scrollTop) {
+			if (!this.autoShowBackToTop) {
+				this.showBackToTopClass = false;
+				return;
+			}
+			if (scrollTop > this.finalBackToTopThreshold) {
+				if (!this.showBackToTopClass) {
+					this.showBackToTopClass = true;
+					this.lastBackToTopShowTime = new Date().getTime();
+					setTimeout(() => {
+						this.backToTopClass = 'zp-back-to-top zp-back-to-top-show';
+					}, 300)
+				}
+			} else {
+				if (this.showBackToTopClass) {
+					this.backToTopClass = 'zp-back-to-top zp-back-to-top-hide';
+					setTimeout(() => {
+						this.showBackToTopClass = false;
+					}, new Date().getTime() - this.lastBackToTopShowTime < 500 ? 0 : 300)
+				}
+			}
+		},
+	}
+}
+

+ 740 - 0
uni_modules/z-paging/components/z-paging/js/modules/data-handle.js

@@ -0,0 +1,740 @@
+// [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, String],
+			default: u.gc('defaultPageNo', 1),
+			observer: function(newVal) {
+				this.pageNo = newVal;
+			},
+		},
+		//自定义pageSize,默认为10
+		defaultPageSize: {
+			type: [Number, String],
+			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: function() {
+				return 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', '')
+		},
+		//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)
+		},
+		//使用聊天记录模式,默认为否
+		useChatRecordMode: {
+			type: Boolean,
+			default: u.gc('useChatRecordMode', false)
+		},
+		//使用聊天记录模式时是否自动隐藏键盘:在用户触摸列表时候自动隐藏键盘,默认为是
+		autoHideKeyboardWhenChat: {
+			type: Boolean,
+			default: u.gc('autoHideKeyboardWhenChat', true)
+		},
+		//自动拼接complete中传过来的数组(使用聊天记录模式时无效)
+		concat: {
+			type: Boolean,
+			default: u.gc('concat', 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: [],
+			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,
+			listRenderingTimeout: null
+		}
+	},
+	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() {
+			if (!this.cacheKey) return null;
+			return `${c.cachePrefixKey}-${this.cacheKey}`; 
+		},
+		isFirstPage() {
+			return this.pageNo === this.defaultPageNo;
+		}
+	},
+	watch: {
+		totalData(newVal, oldVal) {
+			this._totalDataChange(newVal, oldVal);
+		},
+		currentData(newVal, oldVal) {
+			this._currentDataChange(newVal, oldVal);
+		},
+		useChatRecordMode(newVal, oldVal) {
+			if (newVal) {
+				this.nLoadingMoreFixedHeight = false;
+			}
+		},
+		value: {
+			handler(newVal) {
+				this.realTotalData = newVal;
+			},
+			immediate: true
+		},
+		// #ifdef VUE3
+		modelValue: {
+			handler(newVal) {
+				this.realTotalData = newVal;
+			},
+			immediate: true
+		}
+		// #endif
+	},
+	methods: {
+		//请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否成功(默认是是)
+		complete(data, success = true) {
+			this.customNoMore = -1;
+			this.addData(data, success);
+		},
+		//简写,与complete完全相同
+		end(data, success = true) {
+			this.complete(data, success);
+		},
+		//【保证数据一致】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为dataKey,需与:data-key绑定的一致,第三个参数为是否成功(默认为是)
+		completeByKey(data, dataKey = null, success = true) {
+			if (dataKey !== null && this.dataKey !== null && dataKey !== this.dataKey) {
+				if (this.isFirstPage) {
+					this.endRefresh();
+				}
+				return;
+			}
+			this.customNoMore = -1;
+			this.addData(data, success);
+		},
+		//简写,与completeByKey完全相同
+		endByKey(data, dataKey = null, success = true) {
+			this.completeByKey(data, dataKey, success);
+		},
+		//【通过totalCount判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为totalCount(列表总数),第三个参数为是否成功(默认为是)
+		completeByTotalCount(data, totalCount, success = true) {
+			if (totalCount == 'undefined') {
+				this.customNoMore = -1;
+			} else {
+				const dataTypeRes = this._checkDataType(data, success, false);
+				data = dataTypeRes.data;
+				success = dataTypeRes.success;
+				if (totalCount >= 0 && success) {
+					this.$nextTick(() => {
+						let hasMore = true;
+						let realTotalDataCount = this.realTotalData.length;
+						if (this.pageNo == this.defaultPageNo) {
+							realTotalDataCount = 0;
+						}
+						const dataLength = this.privateConcat ? data.length : 0;
+						let exceedCount = realTotalDataCount + dataLength - totalCount;
+						if (exceedCount >= 0) {
+							hasMore = false;
+							exceedCount = this.defaultPageSize - exceedCount;
+							if (exceedCount > 0 && exceedCount < data.length && this.privateConcat) {
+								data = data.splice(0, exceedCount);
+							}
+						}
+						this.completeByNoMore(data, hasMore, success);
+					})
+					return;
+				}
+			}
+			this.addData(data, success);
+		},
+		//简写,与completeByTotalCount完全相同
+		completeByTotal(data, totalCount, success = true) {
+			this.completeByTotalCount(data, totalCount, success);
+		},
+		//简写,与completeByTotalCount完全相同
+		endByTotalCount(data, totalCount, success = true) {
+			this.completeByTotalCount(data, totalCount, success);
+		},
+		//简写,与completeByTotalCount完全相同
+		endByTotal(data, totalCount, success = true) {
+			this.completeByTotalCount(data, totalCount, success);
+		},
+		//【自行判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否有更多数据,第三个参数为是否成功(默认是是)
+		completeByNoMore(data, nomore, success = true) {
+			if (nomore != 'undefined') {
+				this.customNoMore = nomore == true ? 1 : 0;
+			}
+			this.addData(data, success);
+		},
+		//简写,与completeByNoMore完全相同
+		endByNoMore(data, nomore, success = true) {
+			this.completeByNoMore(data, nomore, success);
+		},
+		//与上方complete方法功能一致,新版本中设置服务端回调数组请使用complete方法
+		addData(data, success = true) {
+			if (!this.fromCompleteEmit) {
+				this.disabledCompleteEmit = true;
+				this.fromCompleteEmit = false;
+			}
+			const currentTimeStamp = u.getTime();
+			let addDataDalay = 0;
+			const disTime = currentTimeStamp - this.requestTimeStamp;
+			let minDelay = this.minDelay;
+			if(this.isFirstPage && this.finalShowRefresherWhenReload){
+				minDelay = Math.max(400,minDelay);
+			}
+			if(this.requestTimeStamp > 0 && disTime < minDelay){
+				addDataDalay = minDelay - disTime;
+			}
+			this.$nextTick(() => {
+				let delay = this.delay > 0 ? this.delay : addDataDalay;
+				setTimeout(() => {
+					this._addData(data, success, false);
+				}, delay)
+			})
+		},
+		//从顶部添加数据,不会影响分页的pageNo和pageSize
+		addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
+			let dataType = Object.prototype.toString.call(data);
+			if (dataType !== '[object Array]') {
+				data = [data];
+			}
+			this.totalData = [...data, ...this.totalData];
+			if (toTop) {
+				setTimeout(() => {
+					this._scrollToTop(toTopWithAnimate);
+				}, c.delayTime)
+			}
+		},
+		//重新设置列表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
+		resetTotalData(data) {
+			this.isTotalChangeFromAddData = true;
+			let dataType = Object.prototype.toString.call(data);
+			if (dataType !== '[object Array]') {
+				data = [data];
+			}
+			this.totalData = data;
+		},
+		//添加聊天记录
+		addChatRecordData(data, toBottom = true, toBottomWithAnimate = true) {
+			let dataType = Object.prototype.toString.call(data);
+			if (dataType !== '[object Array]') {
+				data = [data];
+			}
+			if (!this.useChatRecordMode) return;
+			this.isTotalChangeFromAddData = true;
+			//#ifndef APP-NVUE
+			this.totalData = [...this.totalData, ...data];
+			//#endif
+			//#ifdef APP-NVUE
+			this.totalData = this.nIsFirstPageAndNoMore ? [...this.totalData, ...data] : [...data, ...this.totalData];
+			//#endif
+			if (toBottom) {
+				setTimeout(() => {
+					//#ifndef APP-NVUE
+					this._scrollToBottom(toBottomWithAnimate);
+					//#endif
+					//#ifdef APP-NVUE
+					if (this.nIsFirstPageAndNoMore) {
+						this._scrollToBottom(toBottomWithAnimate);
+					} else {
+						this._scrollToTop(toBottomWithAnimate);
+					}
+					//#endif
+				}, c.delayTime)
+			}
+		},
+		//设置本地分页数据,请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging作分页处理(若调用了此方法,则上拉加载更多时内部会自动分页,不会触发@query所绑定的事件)
+		setLocalPaging(data, success = true) {
+			this.isLocalPaging = true;
+			this.$nextTick(() => {
+				this._addData(data, success, true);
+			})
+		},
+		//重新加载分页数据,pageNo会恢复为默认值,相当于下拉刷新的效果(animate为true时会展示下拉刷新动画,默认为false)
+		reload(animate = this.showRefresherWhenReload) {
+			if (animate) {
+				this.privateShowRefresherWhenReload = animate;
+				this.isUserPullDown = true;
+			}
+			this.listRendering = true;
+			this.$nextTick(() => {
+				this._preReload(animate, false);
+			})
+		},
+		//刷新列表数据,pageNo和pageSize不会重置,列表数据会重新从服务端获取。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
+		refresh() {
+			if(!this.realTotalData.length){
+				this.reload();
+				return;
+			}
+			const disPageNo = this.pageNo - this.defaultPageNo + 1;
+			if (disPageNo >= 1) {
+				this.loading = true;
+				this.privateConcat = false;
+				const totalPageSize = disPageNo * this.pageSize;
+				this.currentRefreshPageSize = totalPageSize;
+				this._emitQuery(this.defaultPageNo, totalPageSize, Enum.QueryFrom.Refresh);
+				this._callMyParentQuery(this.defaultPageNo, totalPageSize);
+			}
+		},
+		//手动更新列表缓存数据,将自动截取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();
+		},
+		//手动触发滚动到顶部加载更多,聊天记录模式时有效
+		doChatRecordLoadMore() {
+			this.useChatRecordMode && this._onLoadingMore('click');
+		},
+		//reload之前的一些处理
+		_preReload(animate = this.showRefresherWhenReload, isFromMounted = true) {
+			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++;
+				setTimeout(() => {
+					this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
+						if (node) {
+							let nodeHeight = node[0].height;
+							this.nShowRefresherReveal = true;
+							this.nShowRefresherRevealHeight = nodeHeight;
+							setTimeout(() => {
+								this._nDoRefresherEndAnimation(0, -nodeHeight, false, false);
+								setTimeout(() => {
+									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
+				setTimeout(() => {
+					this._callMyParentQuery();
+				}, delay)
+				if (!isFromMounted && this.autoScrollToTopWhenReload) {
+					let checkedNRefresherLoading = true;
+					// #ifdef APP-NVUE
+					checkedNRefresherLoading = !this.nRefresherLoading;
+					// #endif
+					checkedNRefresherLoading && this._scrollToTop(false);
+				}
+			}
+			this.$nextTick(() => {
+				// #ifdef APP-NVUE
+				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 (tempIsUserPullDown && this.isFirstPage) {
+				this.isUserPullDown = false;
+			}
+			let dataTypeRes = this._checkDataType(data, success, isLocal);
+			data = dataTypeRes.data;
+			success = dataTypeRes.success;
+			let delayTime = c.delayTime;
+			// #ifdef APP-NVUE
+			if (this.useChatRecordMode) delayTime = 0;
+			// #endif
+			this.loadingForNow = false;
+			setTimeout(() => {
+				this.pagingLoaded = true;
+				this.$nextTick(()=>{
+					this._refresherEnd(delayTime > 0, true, tempIsUserPullDown);
+				})
+			}, delayTime)
+			if (this.isFirstPage) {
+				this.isLoadFailed = !success;
+				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.loadingStatus === Enum.More.NoMore)) {
+					this.loadingStatus = Enum.More.Default;
+				}
+				if (isLocal) {
+					this.totalLocalPagingList = data;
+					const localPageNo = this.defaultPageNo;
+					const localPageSize = this.queryFrom !== Enum.QueryFrom.Refresh ? this.defaultPageSize : this.currentRefreshPageSize;
+					this._localPagingQueryList(localPageNo, localPageSize, 0, (res) => {
+						this.complete(res);
+					})
+				} else {
+					let dataChangeDelayTime = 0;
+					// #ifdef APP-NVUE
+					if (this.privateShowRefresherWhenReload && this.finalNvueListIs === 'waterfall') {
+						dataChangeDelayTime = 150;
+					}
+					// #endif
+					setTimeout(() => {
+						this._currentDataChange(data, this.currentData);					
+					}, dataChangeDelayTime)
+				}
+			} else {
+				this._currentDataChange(data, this.currentData);
+				this.loadingStatus = Enum.More.Fail;
+				if (this.loadingType === Enum.LoadingType.LoadingMore) {
+					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;
+			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(() => {
+				setTimeout(()=>{
+					this._getNodeClientRect('.zp-paging-container-content').then((res) => {
+						if (res) {
+							this.$emit('contentHeightChanged', res[0].height);
+						}
+					});
+				},this.isIos ? 100 : 300)
+				// #ifdef APP-NVUE
+				if (this.useChatRecordMode && this.nIsFirstPageAndNoMore && this.isFirstPage && !this.nFirstPageAndNoMoreChecked) {
+					this.nFirstPageAndNoMoreChecked = true;
+					this._scrollToBottom(false);
+				}
+				// #endif
+			})
+		},
+		//当前数据改变时调用
+		_currentDataChange(newVal, oldVal) {
+			newVal = [...newVal];
+			this.listRendering = true;
+			this.listRenderingTimeout && clearTimeout(this.listRenderingTimeout);
+			this.$nextTick(() => {
+				this.listRenderingTimeout = setTimeout(() => {
+					this.listRendering = false;
+				},100)
+			})
+			// #ifndef APP-NVUE
+			this.finalUseVirtualList && this._setCellIndex(newVal,this.totalData.length === 0)
+			this.useChatRecordMode && newVal.reverse();
+			// #endif
+			if (this.isFirstPage && this.finalConcat) {
+				this.totalData = [];
+			}
+			if (this.customNoMore !== -1 && (this.customNoMore === 0 || !newVal.length)) {
+				if (this.customNoMore === 0 || !newVal.length) {
+					this.loadingStatus = Enum.More.NoMore;
+				}
+			} else {
+				if (!newVal.length || (newVal.length && newVal.length < this.defaultPageSize)) {
+					this.loadingStatus = Enum.More.NoMore;
+				}
+			}
+			if (!this.totalData.length) {
+				if (this.finalConcat) {
+					// #ifdef APP-NVUE
+					if(this.useChatRecordMode && this.isFirstPage && this.loadingStatus === Enum.More.NoMore){
+						newVal.reverse();
+					}
+					// #endif
+					this.totalData = newVal;
+				}
+				if (this.useChatRecordMode) {
+					// #ifndef APP-NVUE
+					this.$nextTick(() => {
+						this._scrollToBottom(false);
+					})
+					// #endif
+				}
+			} else {
+				if (this.useChatRecordMode) {
+					// #ifdef APP-NVUE
+					this.totalData = [...this.totalData, ...newVal];
+					// #endif
+					//#ifndef APP-NVUE
+					const idIndex = newVal.length;
+					let idIndexStr = `z-paging-${idIndex}`;
+					this.totalData = [...newVal, ...this.totalData];
+					if (this.pageNo !== this.defaultPageNo) {
+						this.privateScrollWithAnimation = 0;
+						this.$emit('update:chatIndex', idIndex);
+						setTimeout(() => {
+							this._scrollIntoView(idIndexStr, 30 + Math.max(0, this.cacheTopHeight), false, () => {
+								this.$emit('update:chatIndex', 0);
+							});
+						}, this.usePageScroll ? this.isIos ? 50 : 100 : 200)
+					} else {
+						this.$nextTick(() => {
+							this._scrollToBottom(false);
+						})
+					}
+					//#endif
+		
+				} 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;
+		},
+		//本地分页请求
+		_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);
+			setTimeout(() => 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) {
+					if (customPageSize > 0) {
+						this.myParentQuery(customPageNo, customPageSize);
+					} else {
+						this.myParentQuery(this.pageNo, this.defaultPageSize);
+					}
+				}
+			}
+		},
+		//emit query事件
+		_emitQuery(pageNo, pageSize, from){
+			this.queryFrom = from;
+			this.requestTimeStamp = u.getTime();
+			this.$emit('query', ...interceptor._handleQuery(pageNo, pageSize, from));
+		},
+		//检查complete data的类型
+		_checkDataType(data, success, isLocal) {
+			const dataType = Object.prototype.toString.call(data);
+			if (dataType === '[object Boolean]') {
+				success = data;
+				data = [];
+			} else if (dataType === '[object Null]') {
+				data = [];
+			} else if (dataType !== '[object Array]') {
+				data = [];
+				if (dataType !== '[object Undefined]') {
+					u.consoleErr(`${isLocal ? 'setLocalPaging' : 'complete'}参数类型不正确,第一个参数类型必须为Array!`);
+				}
+			}
+			return {data,success};
+		},
+	}
+}

+ 148 - 0
uni_modules/z-paging/components/z-paging/js/modules/empty.js

@@ -0,0 +1,148 @@
+// [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: function() {
+				return u.gc('emptyViewStyle', {});
+			}
+		},
+		//空数据图容器样式
+		emptyViewSuperStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('emptyViewSuperStyle', {});
+			}
+		},
+		//空数据图img样式
+		emptyViewImgStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('emptyViewImgStyle', {});
+			}
+		},
+		//空数据图描述文字样式
+		emptyViewTitleStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('emptyViewTitleStyle', {});
+			}
+		},
+		//空数据图重新加载按钮样式
+		emptyViewReloadStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('emptyViewReloadStyle', {});
+			}
+		},
+		//空数据图片是否铺满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)
+		},
+	},
+	computed: {
+		finalEmptyViewImg() {
+			return this.isLoadFailed ? this.emptyViewErrorImg : this.emptyViewImg;
+		},
+		finalShowEmptyViewReload() {
+			return this.isLoadFailed ? this.showEmptyViewReloadWhenError : this.showEmptyViewReload;
+		},
+		showEmpty() {
+			if (this.refresherOnly || this.hideEmptyView || this.totalData.length) return false;
+			if (this.autoHideEmptyViewWhenLoading) {
+				if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
+			} else {
+				return true;
+			}
+			if (!this.autoHideEmptyViewWhenPull && !this.isUserReload) return true;
+			return false;
+		},
+	},
+	methods: {
+		//点击了空数据view重新加载按钮
+		_emptyViewReload() {
+			let callbacked = false;
+			this.$emit('emptyViewReload', reload => {
+				if (reload === undefined || reload === true) {
+					this.fromEmptyViewReload = true;
+					this.reload();
+				}
+				callbacked = true;
+			});
+			this.$nextTick(() => {
+				if (!callbacked) {
+					this.fromEmptyViewReload = true;
+					this.reload();
+				}
+			})
+		},
+		//点击了空数据view
+		_emptyViewClick() {
+			this.$emit('emptyViewClick');
+		},
+	}
+}

+ 96 - 0
uni_modules/z-paging/components/z-paging/js/modules/i18n.js

@@ -0,0 +1,96 @@
+// [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 {
+	data() {
+		return {
+			language: uni.getSystemInfoSync().language
+		}
+	},
+	computed: {
+		finalLanguage() {
+			const local = uni.getLocale();
+			const language = this.language;
+			return local === 'auto' ? interceptor._handleLanguage2Local(language, this._language2Local(language)) : local;
+		},
+		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')
+			};
+		},
+		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);
+		},
+		finalEmptyViewText() {
+			return this.isLoadFailed ? this.finalEmptyViewErrorText : this._getI18nText('zp.emptyView.title', this.emptyViewText);
+		},
+		finalEmptyViewReloadText() {
+			return this._getI18nText('zp.emptyView.reload', this.emptyViewReloadText);
+		},
+		finalEmptyViewErrorText() {
+			return this._getI18nText('zp.emptyView.error', this.emptyViewErrorText);
+		},
+		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;
+		}
+	}
+}

+ 318 - 0
uni_modules/z-paging/components/z-paging/js/modules/load-more.js

@@ -0,0 +1,318 @@
+// [z-paging]滚动到底部加载更多模块
+import u from '.././z-paging-utils'
+import Enum from '.././z-paging-enum'
+
+export default {
+	props: {
+		//自定义底部加载更多样式
+		loadingMoreCustomStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('loadingMoreCustomStyle', {});
+			}
+		},
+		//自定义底部加载更多文字样式
+		loadingMoreTitleCustomStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('loadingMoreTitleCustomStyle', {});
+			}
+		},
+		//自定义底部加载更多加载中动画样式
+		loadingMoreLoadingIconCustomStyle: {
+			type: Object,
+			default: function() {
+				return 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: function() {
+				return 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,
+			loadingMoreDefaultSlot: null,
+			showLoadingMore: false,
+			customNoMore: -1,
+		}
+	},
+	computed: {
+		zLoadMoreConfig() {
+			return {
+				status: this.loadingStatusAfterRender,
+				defaultAsLoading: this.loadingMoreDefaultAsLoading,
+				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,
+			};
+		},
+		finalLoadingMoreThemeStyle() {
+			return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
+		},
+		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');
+		}
+	},
+	methods: {
+		//页面滚动到底部时通知z-paging进行进一步处理
+		pageReachBottom() {
+			!this.useChatRecordMode && this._onLoadingMore('toBottom');
+		},
+		//手动触发上拉加载更多(非必须,可依据具体需求使用)
+		doLoadMore(type) {
+			this._onLoadingMore(type);
+		},
+		//通过@scroll事件检测是否滚动到了底部
+		_checkScrolledToBottom(scrollDiff, checked = false) {
+			if (this.checkScrolledToBottomTimeOut) {
+				clearTimeout(this.checkScrolledToBottomTimeOut);
+				this.checkScrolledToBottomTimeOut = null;
+			}
+			if (this.cacheScrollNodeHeight === -1) {
+				this._getNodeClientRect('.zp-scroll-view').then((res) => {
+					if (res) {
+						let pageScrollNodeHeight = res[0].height;
+						this.cacheScrollNodeHeight = pageScrollNodeHeight;
+						if (scrollDiff - pageScrollNodeHeight <= this.finalLowerThreshold) {
+							this._onLoadingMore('toBottom');
+						}
+					}
+				});
+			} else {
+				if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
+					this._onLoadingMore('toBottom');
+				} else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
+					this.checkScrolledToBottomTimeOut = setTimeout(() => {
+						this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
+							this.oldScrollTop = res[0].scrollTop;
+							const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
+							this._checkScrolledToBottom(newScrollDiff, true);
+						})
+					}, 150)
+				}
+			}
+		},
+		//触发加载更多时调用,from:0-滑动到底部触发;1-点击加载更多触发
+		_onLoadingMore(from = 'click') {
+			if (from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
+				this.scrollEnable = false;
+				this.$nextTick(() => {
+					this.scrollEnable = true;
+				})
+			}
+			this.$emit('scrolltolower', from);
+			if (from === 'toBottom' && (!this.toBottomLoadingMoreEnabled || this.useChatRecordMode)) return;
+			if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading) return;
+			// #ifdef MP-WEIXIN
+			if (!this.isIos && !this.refresherOnly && !this.usePageScroll) {
+				const currentTimestamp = u.getTime();
+				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) {
+					this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, (res) => {
+						this.addData(res);
+					})
+				} else {
+					this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadingMore);
+					this._callMyParentQuery();
+				}
+				this.loadingType = Enum.LoadingType.LoadingMore;
+			}
+		},
+		//(预处理)判断当没有更多数据且分页内容未超出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;
+						this.insideOfPaging = scrollViewTotalH < this.windowHeight;
+						if (this.hideNoMoreInside) {
+							this.showLoadingMore = !this.insideOfPaging;
+						}
+						this._updateInsideOfPaging();
+					}
+				} else {
+					const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
+					const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
+					const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
+					this.insideOfPaging = pagingContainerH < scrollViewH;
+					if (this.hideNoMoreInside) {
+						this.showLoadingMore = !this.insideOfPaging;
+					}
+					this._updateInsideOfPaging();
+				}
+			} catch (e) {
+				this.insideOfPaging = !totalData.length;
+				if (this.hideNoMoreInside) {
+					this.showLoadingMore = !this.insideOfPaging;
+				}
+				this._updateInsideOfPaging();
+			}
+		},
+		//是否要展示上拉加载更多view
+		_showLoadingMore(type) {
+			if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.totalData.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.$slots) return false;
+			if (type === 'Custom') {
+				return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
+			}
+			const res = this.loadingStatus === Enum.More[type] && this.$slots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
+			if (res) {
+				// #ifdef APP-NVUE
+				if (!this.isIos) {
+					this.nLoadingMoreFixedHeight = false;
+				}
+				//  #endif
+			}
+			return res;
+		},
+	}
+}

+ 93 - 0
uni_modules/z-paging/components/z-paging/js/modules/loading.js

@@ -0,0 +1,93 @@
+// [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: {
+		loadingStatus(newVal) {
+			this.$emit('loadingStatusChange', newVal);
+			this.$nextTick(()=>{
+				this.loadingStatusAfterRender = newVal;
+			})
+			// #ifdef APP-NVUE
+			if (this.useChatRecordMode) {
+				if (this.pageNo === this.defaultPageNo && newVal === Enum.More.NoMore) {
+					this.nIsFirstPageAndNoMore = true;
+					return;
+				}
+			}
+			this.nIsFirstPageAndNoMore = false;
+			//  #endif
+		},
+		loading(newVal){
+			if (newVal) {
+				this.loadingForNow = newVal;
+			}
+		},
+	},
+	computed: {
+		showLoading() {
+			if (this.firstPageLoaded || !this.loading || !this.loadingForNow) return false;
+			if (this.finalShowSystemLoading){
+				uni.showLoading({
+					title: this.finalSystemLoadingText,
+					mask: this.systemLoadingMask
+				})
+			}
+			return this.autoHideLoadingAfterFirstLoaded ? (this.fromEmptyViewReload ? true : !this.pagingLoaded) : this.loadingType === Enum.LoadingType.Refresher;
+		},
+		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
+		}
+	}
+}

+ 249 - 0
uni_modules/z-paging/components/z-paging/js/modules/nvue.js

@@ -0,0 +1,249 @@
+// [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: function() {
+				return 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: function() {
+				return 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,
+			nIsFirstPageAndNoMore: false,
+			nFirstPageAndNoMoreChecked: false,
+			nLoadingMoreFixedHeight: false,
+			nShowRefresherRevealHeight: 0,
+			nOldShowRefresherRevealHeight: -1,
+			nRefresherWidth: uni.upx2px(750),
+		}
+	},
+	watch: {
+		nIsFirstPageAndNoMore: {
+			handler(newVal) {
+				const cellStyle = !this.useChatRecordMode || newVal ? {} : {transform: 'rotate(180deg)'};
+				this.$emit('update:cellStyle', cellStyle);
+				this.$emit('cellStyleChange', cellStyle);
+			},
+			immediate: true
+		}
+	},
+	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;
+		},
+		nChatRecordRotateStyle() {
+			return this.useChatRecordMode ? { transform: this.nIsFirstPageAndNoMore ? 'rotate(0deg)' : 'rotate(180deg)' } : {};
+		},
+		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
+	},
+	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);
+		},
+		//列表开始触摸
+		_nTouchstart() {
+			this._handleListTouchstart();
+		},
+		//下拉刷新刷新中
+		_nOnRrefresh() {
+			if (this.nShowRefresherReveal) return;
+			this.nRefresherLoading = true;
+			this.refresherStatus = Enum.Refresher.Loading;
+			this._doRefresherLoad();
+		},
+		//下拉刷新下拉中
+		_nOnPullingdown(e) {
+			if (this.refresherStatus === Enum.Refresher.Loading || (this.isIos && !this.nListIsDragging)) return;
+			this._emitTouchmove(e);
+			const viewHeight = e.viewHeight;
+			const pullingDis = e.pullingDistance;
+			this.refresherStatus = pullingDis >= viewHeight ? Enum.Refresher.ReleaseToRefresh : Enum.Refresher.Default;
+		},
+		//下拉刷新结束
+		_nRefresherEnd(doEnd = true) {
+			if (doEnd) {
+			   this._nDoRefresherEndAnimation(0, -this.nShowRefresherRevealHeight); 
+			   !this.usePageScroll && this.$refs['zp-n-list'].resetLoadmore();
+			   this.nRefresherLoading = false;
+			}
+			this.$nextTick(() => {
+				setTimeout(()=> {
+					this.nShowBottom = true;
+				}, 10);
+			})
+		},
+		//执行主动触发下拉刷新动画
+		_nDoRefresherEndAnimation(height, translateY, animate = true, checkStack = true) {
+			this._cleanRefresherCompleteTimeout();
+			this._cleanRefresherEndTimeout();
+			
+			if (!this.finalShowRefresherWhenReload) {
+				this.refresherEndTimeout = setTimeout(() => {
+					this.refresherStatus = Enum.Refresher.Default;
+				}, this.refresherCompleteDuration);
+				return;
+			}
+			const stackCount = this.refresherRevealStackCount;
+			if (height === 0 && checkStack) {
+				this.refresherRevealStackCount --;
+				if (stackCount > 1) return;
+				this.refresherEndTimeout = setTimeout(() => {
+					this.refresherStatus = Enum.Refresher.Default;
+				}, this.refresherCompleteDuration);
+			}
+			if (stackCount > 1) {
+				this.refresherStatus = Enum.Refresher.Loading;
+			}
+			
+			const duration = animate ? 180 : 0;
+			if (this.nOldShowRefresherRevealHeight !== height) {
+				if(height > 0){
+					this.nShowRefresherReveal = true;
+				}
+				weexAnimation.transition(this.$refs['zp-n-list-refresher-reveal'], {
+					styles: {
+						height: `${height}px`,
+						transform: `translateY(${translateY}px)`,
+					},
+					duration: duration,
+					timingFunction: 'linear',
+					needLayout: true,
+					delay: 0
+				})
+			}
+			setTimeout(() => {
+				if (animate) {
+					this.nShowRefresherReveal = height > 0;
+				}
+			}, duration > 0 ? duration - 100 : 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() {
+			setTimeout(() => {
+				this.$nextTick(()=>{
+					this._getNodeClientRect('.zp-n-list').then(node => {
+						if (node) {
+							this.nRefresherWidth = node[0].width ? node[0].width : this.nRefresherWidth;
+						}
+					})
+				})
+			},c.delayTime)	
+		}
+		// #endif
+	}
+}

+ 662 - 0
uni_modules/z-paging/components/z-paging/js/modules/refresher.js

@@ -0,0 +1,662 @@
+// [z-paging]下拉刷新view模块
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import Enum from '.././z-paging-enum'
+
+export default {
+	props: {
+		//下拉刷新的主题样式,支持black,white,默认black
+		refresherThemeStyle: {
+			type: String,
+			default: u.gc('refresherThemeStyle', '')
+		},
+		//自定义下拉刷新中左侧图标的样式
+		refresherImgStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('refresherImgStyle', {});
+			}
+		},
+		//自定义下拉刷新中右侧状态描述文字的样式
+		refresherTitleStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('refresherTitleStyle', {});
+			}
+		},
+		//自定义下拉刷新中右侧最后更新时间文字的样式(show-refresher-update-time为true时有效)
+		refresherUpdateTimeStyle: {
+			type: Object,
+			default: function() {
+				return 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)
+		},
+		//自定义下拉刷新结束状态下是否允许列表滚动,默认为否
+		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)
+		},
+		//自定义下拉刷新默认状态下的图片
+		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)
+		},
+		//是否开启自定义下拉刷新刷新结束回弹效果,默认为是
+		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)
+		},
+		//设置自定义下拉刷新下拉时实际下拉位移与用户下拉距离的比值,默认为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)
+		},
+	},
+	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,
+			pullDownTimeStamp: 0,
+			moveDis: 0,
+			oldMoveDis: 0,
+			currentDis: 0,
+			oldCurrentMoveDis: 0,
+			oldRefresherTouchmoveY: 0,
+			oldTouchDirection: ''
+		}
+	},
+	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 && this._doVibrateShort();
+			this.$emit('refresherStatusChange', newVal);
+			this.$emit('update:refresherStatus', newVal);
+		}
+	},
+	computed: {
+		pullDownDisTimeStamp() {
+			return 1000 / this.refresherFps;
+		},
+		finalRefresherEnabled() {
+			if (this.useChatRecordMode) return false;
+			if (this.privateRefresherEnabled === -1) return this.refresherEnabled;
+			return this.privateRefresherEnabled === 1;
+		},
+		finalRefresherThreshold() {
+			let refresherThreshold = this.refresherThreshold;
+			let idDefault = false;
+			if (refresherThreshold === '80rpx') {
+				idDefault = true;
+				if (this.showRefresherUpdateTime) {
+					refresherThreshold = '120rpx';
+				}
+			}
+			if (idDefault && this.customRefresherHeight > 0) return this.customRefresherHeight;
+			return u.convertTextToPx(refresherThreshold);
+		},
+		finalRefresherFixedBacHeight() {
+			return u.convertTextToPx(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.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;
+			// #ifndef APP-NVUE
+			if (this.customRefresherHeight === -1 && showRefresher) {
+				setTimeout(() => {
+					this.$nextTick(()=>{
+						this._updateCustomRefresherHeight();
+					})
+				}, 100)
+			}
+			// #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();
+		},
+		handleRefresherStatusChanged(func) {
+			this.refresherStatusChangedFunc = func;
+		},
+		//自定义下拉刷新被触发
+		_onRefresh(fromScrollView = false,isUserPullDown = true) {
+			if (fromScrollView && !(this.finalRefresherEnabled && !this.useCustomRefresher)) return;
+			this.$emit('onRefresh');
+			this.$emit('Refresh');
+			// #ifdef APP-NVUE
+			if (this.loading) {
+				setTimeout(()=>{
+					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
+		//拖拽开始
+		_refresherTouchstart(e) {
+			this._handleListTouchstart();
+			if (this._touchDisabled()) return;
+			const touch = u.getTouch(e);
+			this._handleRefresherTouchstart(touch);
+		},
+		// #endif
+		//进一步处理拖拽开始结果
+		_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();
+		},
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		//拖拽中
+		_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';
+				direction === this.oldTouchDirection && this._handleTouchDirectionChange({direction});
+				this.oldTouchDirection = direction;
+				this.oldRefresherTouchmoveY = refresherTouchmoveY;
+			}
+			if (this.pullDownTimeStamp && currentTimeStamp - this.pullDownTimeStamp <= this.pullDownDisTimeStamp) return;
+			if (this._touchDisabled()) return;
+			this.pullDownTimeStamp = Number(currentTimeStamp);
+			touch = u.getTouch(e);
+			refresherTouchmoveY = touch.touchY;
+			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;
+					if (angle < this.refresherMaxAngle) {
+						this.lastRefresherTouchmove = touch;
+						this.refresherReachMaxAngle = false;
+						return;
+					}
+				}
+			}
+			moveDis = this._getFinalRefresherMoveDis(moveDis);
+			this._handleRefresherTouchmove(moveDis, touch);
+			if (!this.disabledBounce) {
+				if(this.isIos){
+					// #ifndef MP-LARK
+					this._handleScrollViewDisableBounce({bounce: false});
+					// #endif
+				}
+				this.disabledBounce = true;
+			}
+			this._emitTouchmove({pullingDistance:moveDis,dy:this.moveDis - this.oldMoveDis});
+		},
+		// #endif
+		//进一步处理拖拽中结果
+		_handleRefresherTouchmove(moveDis, touch) {
+			this.refresherReachMaxAngle = true;
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this.isTouchmoving = true;
+			this.isTouchEnded = false;
+			this.refresherStatus = moveDis >= this.finalRefresherThreshold ? Enum.Refresher.ReleaseToRefresh : this.refresherStatus = Enum.Refresher.Default;
+			// #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
+			// this.scrollEnable = false;
+			this.refresherTransform = `translateY(${moveDis}px)`;
+			this.lastRefresherTouchmove = touch;
+			// #endif
+			this.moveDis = moveDis;
+		},
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		//拖拽结束
+		_refresherTouchend(e) {
+			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._handleScrollViewDisableBounce({bounce: true});
+			this.disabledBounce = false;
+		},
+		// #endif
+		//进一步处理拖拽结束结果
+		_handleRefresherTouchend(moveDis) {
+			// #ifndef APP-PLUS || H5 || MP-WEIXIN
+			if (!this.isTouchmoving) return;
+			// #endif
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this.refresherReachMaxAngle = true;
+			this.isTouchEnded = true;
+			if (moveDis >= this.finalRefresherThreshold && this.refresherStatus === Enum.Refresher.ReleaseToRefresh) {
+				// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+				this.refresherTransform = `translateY(${this.finalRefresherThreshold}px)`;
+				this.refresherTransition = 'transform .1s linear';
+				// #endif
+				this.moveDis = this.finalRefresherThreshold;
+				this.refresherStatus = Enum.Refresher.Loading;
+				this._doRefresherLoad();
+			} else {
+				this._refresherEnd();
+				this.isTouchmovingTimeout = setTimeout(() => {
+					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是否生效
+		_handleScrollViewDisableBounce({ bounce }) {
+			if (!this.usePageScroll && !this.scrollToTopBounceEnabled) {
+				// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
+				this.refresherTransition = '';
+				// #endif
+				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) {
+				let refresherCompleteDelay = 0;
+				if(fromAddData && (isUserPullDown || this.showRefresherWhenReload)){
+					refresherCompleteDelay = this.refresherCompleteDuration > 700 ? 1 : this.refresherCompleteDelay;
+				}
+				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 = setTimeout(() => {
+					this.refresherStatus = refresherStatus;
+				}, 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 = setTimeout(() => {
+					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 = setTimeout(() => {
+							this.$nextTick(() => {
+								this.refresherStatus = Enum.Refresher.Default;
+								this.isRefresherInComplete = false;
+							})
+						}, animateDuration * 800);
+					}
+					// #endif
+				}, refresherCompleteDelay);
+			}
+			if (setLoading) {
+				setTimeout(() => {
+					this.loading = false;
+				}, shouldEndLoadingDelay ? c.delayTime : 0);
+				isUserPullDown && this._onRestore();
+			}
+		},
+		//模拟用户手动触发下拉刷新
+		_doRefresherRefreshAnimate() {
+			this._cleanRefresherCompleteTimeout();
+			// #ifndef APP-NVUE
+			const doRefreshAnimateAfter = !this.doRefreshAnimateAfter && (this.finalShowRefresherWhenReload) && this
+				.customRefresherHeight === -1 && this.refresherThreshold === '80rpx';
+			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 * this.finalRefresherPullRate;
+				if (this.currentDis > this.finalRefresherThreshold) {
+					diffDis = diffDis * (1 - this.finalRefresherOutRate);
+				}
+			}
+			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();
+				}
+			});
+		},
+		//发射pullingDown事件
+		_emitTouchmove(e){
+			// #ifndef APP-NVUE
+			e.viewHeight = this.finalRefresherThreshold;
+			// #endif
+			e.rate = e.pullingDistance / e.viewHeight;
+			this.hasTouchmove && this.$emit('refresherTouchmove',e);
+		},
+		//清除refresherCompleteTimeout
+		_cleanRefresherCompleteTimeout() {
+			this.refresherCompleteTimeout = this._cleanTimeout(this.refresherCompleteTimeout);
+			// #ifdef APP-NVUE
+			this._nRefresherEnd(false);
+			// #endif
+		},
+		//清除refresherEndTimeout
+		_cleanRefresherEndTimeout() {
+			this.refresherEndTimeout = this._cleanTimeout(this.refresherEndTimeout);
+		},
+	}
+}

+ 445 - 0
uni_modules/z-paging/components/z-paging/js/modules/scroller.js

@@ -0,0 +1,445 @@
+// [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,
+			scrollViewStyle: {},
+			scrollViewContainerStyle: {},
+			scrollViewInStyle: {},
+			pageScrollTop: -1,
+			scrollEnable: true,
+			privateScrollWithAnimation: -1,
+			cacheScrollNodeHeight: -1
+		}
+	},
+	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) {
+			if (!this.useChatRecordMode) {
+				this.renderPropScrollTop = newVal < 6 ? 0 : 10;
+			}
+		},
+	},
+	computed: {
+		finalScrollWithAnimation() {
+			if (this.privateScrollWithAnimation !== -1) {
+				const scrollWithAnimation = this.privateScrollWithAnimation === 1;
+				this.privateScrollWithAnimation = -1;
+				return scrollWithAnimation;
+			}
+			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;
+		},
+		finalIsOldWebView() {
+			return this.isOldWebView && !this.usePageScroll;
+		}
+	},
+	methods: {
+		//滚动到顶部,animate为是否展示滚动动画,默认为是
+		scrollToTop(animate, checkReverse = true) {
+			// #ifdef APP-NVUE
+			if (checkReverse && this.useChatRecordMode) {
+				if(!this.nIsFirstPageAndNoMore){
+					this.scrollToBottom(animate, false);
+					return;
+				}
+			}
+			// #endif
+			this.$nextTick(() => {
+				this._scrollToTop(animate, false);
+				// #ifdef APP-NVUE
+				if (this.nvueFastScroll && animate) {
+					setTimeout(() => {
+						this._scrollToTop(false, false);
+					}, 150);
+				}
+				// #endif
+			})
+		},
+		//滚动到底部,animate为是否展示滚动动画,默认为是
+		scrollToBottom(animate, checkReverse = true) {
+			// #ifdef APP-NVUE
+			if (checkReverse && this.useChatRecordMode) {
+				if(!this.nIsFirstPageAndNoMore){
+					this.scrollToTop(animate, false);
+					return;
+				}
+			}
+			// #endif
+			this.$nextTick(() => {
+				this._scrollToBottom(animate);
+				// #ifdef APP-NVUE
+				if (this.nvueFastScroll && animate) {
+					setTimeout(() => {
+						this._scrollToBottom(false);
+					}, 150);
+				}
+				// #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);
+			})
+		},
+		//滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollToY(y, offset, animate) {
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this._scrollToY(y, offset, animate);
+			})
+		},
+		//滚动到指定view(nvue中有效)。index为需要滚动的view的index(第几个);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollIntoViewByIndex(index, offset, animate) {
+			this._scrollIntoView(index, offset, animate);
+		},
+		//滚动到指定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() {
+			this.$nextTick(() => {
+				this._updateLeftAndRightWidth();
+			})
+		},
+		//更新z-paging内置scroll-view的scrollTop
+		updateScrollViewScrollTop(scrollTop, animate = true) {
+			this.privateScrollWithAnimation = animate ? 1 : 0;
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this.scrollTop = scrollTop;
+				this.oldScrollTop = this.scrollTop;
+			});
+		},
+		
+		//当滚动到顶部时
+		_scrollToUpper() {
+			this.$emit('scrolltoupper');
+			this.$emit('scrollTopChange', 0);
+			this.$nextTick(() => {
+				this.oldScrollTop = 0;
+			})
+			if (!this.useChatRecordMode) return;
+			if (this.loadingStatus === Enum.More.NoMore) return;
+			this._onLoadingMore('click');
+		},
+		//滚动到顶部
+		_scrollToTop(animate = true, isPrivate = true) {
+			// #ifdef APP-NVUE
+			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.privateScrollWithAnimation = animate ? 1 : 0;
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this.scrollTop = 0;
+				this.oldScrollTop = this.scrollTop;
+			});
+		},
+		//滚动到底部
+		async _scrollToBottom(animate = true) {
+			// #ifdef APP-NVUE
+			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.privateScrollWithAnimation = animate ? 1 : 0;
+				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
+					if (sel.indexOf('#') != -1) {
+						sel = sel.replace('#', '');
+					}
+					this._getNodeClientRect('#' + sel, false).then((node) => {
+						if (node) {
+							let nodeTop = node[0].top;
+							this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
+							if (finishCallback) {
+								finishCallback();
+							}
+						}
+					});
+				});
+			} catch (e) {}
+		},
+		//通过nodeTop滚动到指定view
+		_scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
+			this._scrollToY(nodeTop, offset, animate, true);
+		},
+		//滚动到指定位置
+		_scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
+			this.privateScrollWithAnimation = animate ? 1 : 0;
+			if (this.usePageScroll) {
+				uni.pageScrollTo({
+					scrollTop: y - offset,
+					duration: animate ? 100 : 0
+				});
+			} else {
+				if(addScrollTop){
+				   y += this.oldScrollTop; 
+				}
+				this.scrollTop = y - offset;
+				this.oldScrollTop = this.scrollTop;
+			}
+		},
+		//scroll-view滚动中
+		_scroll(e) {
+			this.$emit('scroll', e);
+			const scrollTop = e.detail.scrollTop;
+			// #ifndef APP-NVUE
+			this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
+			// #endif
+			this.oldScrollTop = scrollTop;
+			const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
+			!this.isIos && this._checkScrolledToBottom(scrollDiff);
+		},
+		//scrollTop改变时触发
+		_scrollTopChange(newVal, isPageScrollTop){
+			this.$emit('scrollTopChange', newVal);
+			this.$emit('update:scrollTop', newVal);
+			this._checkShouldShowBackToTop(newVal);
+			const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : newVal;
+			if (isPageScrollTop) {
+				this.wxsPageScrollTop = scrollTop;
+			} else {
+				this.wxsScrollTop = scrollTop;
+			}
+		},
+		//更新使用页面滚动时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
+				setTimeout(() => {
+					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)
+			})
+		},
+		//获取slot="left"和slot="right"宽度并且更新布局
+		_updateLeftAndRightWidth(){
+			if (!this.finalIsOldWebView) return;
+			this.$nextTick(() => {
+				let delayTime = 0;
+				// #ifdef MP-BAIDU
+				delayTime = 10;
+				// #endif
+				setTimeout(() => {
+					['left','right'].map(position => {
+						this._getNodeClientRect(`.zp-page-${position}`).then((res) => {
+							this.$set(this.scrollViewContainerStyle, position, res ? res[0].width + 'px' : '0px');
+						});
+					})
+				}, delayTime)
+			})
+		}
+	}
+}

+ 400 - 0
uni_modules/z-paging/components/z-paging/js/modules/virtual-list.js

@@ -0,0 +1,400 @@
+// [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: function() {
+				return 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: function() {
+				return u.gc('innerListStyle', {});
+			}
+		},
+		//innerCell样式
+		innerCellStyle: {
+			type: Object,
+			default: function() {
+				return u.gc('innerCellStyle', {});
+			}
+		},
+		//预加载的列表可视范围(列表高度)页数,默认为7,即预加载当前页及上下各7页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
+		preloadPage: {
+			type: [Number, String],
+			default: u.gc('preloadPage', 7),
+			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)
+		},
+		//虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
+		virtualListCol: {
+			type: [Number, String],
+			default: u.gc('virtualListCol', 1)
+		},
+		//虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
+		virtualScrollFps: {
+			type: [Number, String],
+			default: u.gc('virtualScrollFps', 80)
+		},
+	},
+	data() {
+		return {
+			virtualListKey: u.getInstanceId(),
+			virtualPageHeight: 0,
+			virtualCellHeight: 0,
+			virtualScrollTimeStamp: 0,
+			
+			virtualList: [],
+			virtualPlaceholderTopHeight: 0,
+			virtualPlaceholderBottomHeight: 0,
+			virtualTopRangeIndex: 0,
+			virtualBottomRangeIndex: 0,
+			lastVirtualTopRangeIndex: 0,
+			lastVirtualBottomRangeIndex: 0,
+			
+			virtualHeightCacheList: [],
+			
+			getCellHeightRetryCount: {
+				fixed: 0,
+				dynamic: 0
+			},
+			pagingOrgTop: -1,
+			updateVirtualListFromDataChange: false
+		}
+	},
+	watch: {
+		realTotalData(newVal) {
+			// #ifndef APP-NVUE
+			if (this.finalUseVirtualList) {
+				this.updateVirtualListFromDataChange = true;
+				this.$nextTick(() => {
+					if (!newVal.length) {
+						this._resetDynamicListState(!this.isUserPullDown);
+					}
+					this.getCellHeightRetryCount.fixed = 0;
+					newVal.length && this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight();
+					this._updateVirtualScroll(this.oldScrollTop);
+				})
+			}
+			// #endif
+		},
+		virtualList(newVal){
+			this.$emit('update:virtualList', newVal);
+			this.$emit('virtualListChange', newVal);
+		}
+	},
+	computed: {
+		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;
+			return virtualPageHeight * this.preloadPage;
+		},
+		virtualRangePageHeight(){
+			return this.finalVirtualPageHeight * this.preloadPage;
+		},
+		virtualScrollDisTimeStamp() {
+			return 1000 / this.virtualScrollFps;
+		},
+	},
+	methods: {
+		//在使用动态高度虚拟列表时,手动更新指定cell的缓存高度(当cell高度在初始化之后再次改变时调用),index代表需要更新的cell在列表中的位置,从0开始
+		didUpdateVirtualListCell(index) {
+			if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
+			const currentNode = this.virtualHeightCacheList[index];
+			this._getNodeClientRect(`#zp-id-${index}`,this.finalUseInnerList).then(cellNode => {
+				const cellNodeHeight = cellNode ? cellNode[0].height : 0;
+				
+				const heightDis = cellNodeHeight - currentNode.height;
+				currentNode.height = cellNodeHeight;
+				currentNode.totalHeight = currentNode.lastHeight + cellNodeHeight;
+				
+				for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
+					const thisNode = this.virtualHeightCacheList[i];
+					if (i === index + 1) {
+						thisNode.lastHeight = cellNodeHeight;
+					}
+					thisNode.totalHeight += heightDis;
+				}
+			});
+		},
+		//在使用动态高度虚拟列表时,若删除了列表数组中的某个item,需要调用此方法以更新高度缓存数组,index代表需要更新的cell在列表中的位置,从0开始
+		didDeleteVirtualListCell(index) {
+			if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
+			const currentNode = this.virtualHeightCacheList[index];
+			for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
+				const thisNode = this.virtualHeightCacheList[i];
+				if (i === index + 1) {
+					thisNode.lastHeight = currentNode.lastHeight;
+				}
+				thisNode.totalHeight -= currentNode.height;
+			}
+			this.virtualHeightCacheList.splice(index, 1);
+		},
+		//初始化虚拟列表
+		_virtualListInit() {
+			this.$nextTick(() => {
+				setTimeout(() => {
+					this._getNodeClientRect('.zp-scroll-view').then(node => {
+						if (node) {
+							this.pagingOrgTop = node[0].top;
+							this.virtualPageHeight = node[0].height;
+						}
+					});
+				}, c.delayTime);
+			})
+		},
+		//cellHeightMode为fixed时获取第一个cell高度
+		_updateFixedCellHeight() {
+			this.$nextTick(() => {
+				const updateFixedCellHeightTimeout = setTimeout(() => {
+					this._getNodeClientRect(`#zp-id-${0}`,this.finalUseInnerList).then(cellNode => {
+						if (!cellNode) {
+							clearTimeout(updateFixedCellHeightTimeout);
+							if (this.getCellHeightRetryCount.fixed > 10) return;
+							this.getCellHeightRetryCount.fixed ++;
+							this._updateFixedCellHeight();
+						} else {
+							this.virtualCellHeight = cellNode[0].height;
+							this._updateVirtualScroll(this.oldScrollTop);
+						}
+					});
+				}, c.delayTime);
+			})
+		},
+		//cellHeightMode为dynamic时获取每个cell高度
+		_updateDynamicCellHeight(list) {
+			this.$nextTick(() => {
+				const updateDynamicCellHeightTimeout = setTimeout(async () => {
+					for (let i = 0; i < list.length; i++) {
+						let item = list[i];
+						const cellNode = await this._getNodeClientRect(`#zp-id-${item[c.listCellIndexKey]}`,this.finalUseInnerList);
+						const currentHeight = cellNode ? cellNode[0].height : 0;
+						if (!cellNode) {
+							clearTimeout(updateDynamicCellHeightTimeout);
+							this.virtualHeightCacheList = this.virtualHeightCacheList.slice(-i);
+							if (this.getCellHeightRetryCount.dynamic > 10) return;
+							this.getCellHeightRetryCount.dynamic++;
+							this._updateDynamicCellHeight(list);
+							break;
+						}
+						const lastHeightCache = this.virtualHeightCacheList.length ? this.virtualHeightCacheList.slice(-1)[0] : null;
+						const lastHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
+						this.virtualHeightCacheList.push({
+							height: currentHeight,
+							lastHeight: lastHeight,
+							totalHeight: lastHeight + currentHeight
+						});
+					}
+					this._updateVirtualScroll(this.oldScrollTop);
+				}, c.delayTime)
+			})
+		},
+		//设置cellItem的index
+		_setCellIndex(list, isFirstPage) {
+			let lastItemIndex = 0;
+			if (!isFirstPage) {
+				lastItemIndex = this.realTotalData.length;
+				const lastItem = this.realTotalData.length ? this.realTotalData.slice(-1)[0] : null;
+				if (lastItem && lastItem[c.listCellIndexKey] !== undefined) {
+					lastItemIndex = lastItem[c.listCellIndexKey] + 1;
+				}
+			} 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};
+				}
+				item[c.listCellIndexKey] = lastItemIndex + i;
+				item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[c.listCellIndexKey]}`;
+				list[i] = item;
+			}
+			this.getCellHeightRetryCount.dynamic = 0;
+			this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list);
+		},
+		//更新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) {
+				scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
+				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;
+				if (scrollDirection === 'bottom') {
+					for (let i = startTopRangeIndex; i < heightCacheList.length;i++){
+						const heightCacheItem = heightCacheList[i];
+						if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
+							this.virtualTopRangeIndex = i;
+							this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
+							break;
+						}
+					}
+				} else {
+					let topRangeMatched = false;
+					for (let i = startTopRangeIndex; i >= 0;i--){
+						const heightCacheItem = heightCacheList[i];
+						if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
+							this.virtualTopRangeIndex = i;
+							this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
+							topRangeMatched = true;
+							break;
+						}
+					}
+					!topRangeMatched && this._resetTopRange();
+				}
+				for (let i = this.virtualTopRangeIndex; i < heightCacheList.length;i++){
+					const heightCacheItem = heightCacheList[i];
+					if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
+						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) * 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) * (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);
+						}
+					});
+				})
+			}
+		},
+		//处理使用内置列表时点击了cell事件
+		_innerCellClick(item, index) {
+			this.$emit('innerCellClick', item, index);
+		}
+	}
+}

+ 7 - 18
uni_modules/z-paging/components/z-paging/js/z-paging-config.js

@@ -1,32 +1,21 @@
-// z-paging
-// github地址:https://github.com/SmileZXLee/uni-z-paging
-// dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-// 反馈QQ群:790460711
-// z-paging配置文件
+// [z-paging]处理main.js中的配置信息工具
 
 let config = null;
 let getedStorage = false;
 const storageKey = 'Z-PAGING-CONFIG-STORAGE-KEY'
 
 function setConfig(value) {
-	try {
-		uni.setStorageSync(storageKey, value);
-	} catch {}
+	uni.setStorageSync(storageKey, value);
 }
 
 function getConfig() {
-	try {
-		if (getedStorage) {
-			return config;
-		}
-		config = uni.getStorageSync(storageKey);
-		getedStorage = true;
-	} catch {
-		return null;
-	}
+	if (getedStorage) return config;
+	config = uni.getStorageSync(storageKey);
+	getedStorage = true;
+	return config;
 }
 
-module.exports = {
+export default {
 	setConfig,
 	getConfig
 };

+ 12 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-constant.js

@@ -0,0 +1,12 @@
+// [z-paging]常量
+
+export default {
+	version: '2.4.8',
+	delayTime: 100,
+	errorUpdateKey: 'z-paging-error-emit',
+	completeUpdateKey: 'z-paging-complete-emit',
+	cachePrefixKey: 'z-paging-cache',
+	
+	listCellIndexKey: 'zp_index',
+	listCellIndexUniqueKey: 'zp_unique_index'
+}

+ 44 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-enum.js

@@ -0,0 +1,44 @@
+// [z-paging]枚举
+
+export default {
+	//当前加载类型 0.下拉刷新 1.上拉加载更多
+	LoadingType: {
+		Refresher: 0,
+		LoadingMore: 1
+	},
+	//下拉刷新状态 0.默认状态 1.松手立即刷新 2.刷新中 3.刷新结束
+	Refresher: {
+		Default: 0,
+		ReleaseToRefresh: 1,
+		Loading: 2,
+		Complete: 3
+	},
+	//底部加载更多状态 0.默认状态 1.加载中 2.没有更多数据 3.加载失败
+	More: {
+		Default: 0,
+		Loading: 1,
+		NoMore: 2,
+		Fail: 3
+	},
+	//@query触发来源 0.用户主动下拉刷新 1.通过reload触发 2.通过refresh触发 3.通过滚动到底部加载更多或点击底部加载更多触发
+	QueryFrom: {
+		UserPullDown: 0,
+		Reload: 1,
+		Refresh: 2,
+		LoadingMore: 3
+	},
+	//虚拟列表cell高度模式
+	CellHeightMode: {
+		//固定高度
+		Fixed: 'fixed',
+		//动态高度
+		Dynamic: 'dynamic'
+	},
+	//列表缓存模式
+	CacheMode: {
+		//默认模式,只会缓存一次
+		Default: 'default',
+		//总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
+		Always: 'always'
+	}
+}

+ 54 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-interceptor.js

@@ -0,0 +1,54 @@
+// [z-paging]拦截器
+
+//拦截&处理@query事件
+function handleQuery(callback) {
+	try {
+		setTimeout(function() {
+			_getApp().globalData.zp_handleQueryCallback = callback;
+		}, 1);
+	} catch (e) {}
+}
+
+//拦截&处理@query事件(私有,请勿调用)
+function _handleQuery(pageNo, pageSize, from){
+	const handleQueryCallback = _getApp().globalData.zp_handleQueryCallback;
+	if (handleQueryCallback) {
+		return handleQueryCallback(pageNo, pageSize, from);
+	}
+	return [pageNo, pageSize, from];
+}
+
+//拦截&处理系统language转i18n local
+function handleLanguage2Local(callback) {
+	try {
+		setTimeout(function() {
+			_getApp().globalData.zp_handleLanguage2LocalCallback = callback;
+		}, 1);
+	} catch (e) {}
+}
+
+//拦截&处理系统language转i18n local(私有,请勿调用)
+function _handleLanguage2Local(language, local){
+	const handleLanguage2LocalCallback = _getApp().globalData.zp_handleLanguage2LocalCallback;
+	if (handleLanguage2LocalCallback) {
+		return handleLanguage2LocalCallback(language, local);
+	}
+	return local;
+}
+
+//获取当前app对象
+function _getApp(){
+	// #ifndef APP-NVUE
+	return getApp();
+	// #endif
+	// #ifdef APP-NVUE
+	return getApp({allowDefault: true});
+	// #endif
+}
+
+export default {
+	handleQuery,
+	_handleQuery,
+	handleLanguage2Local,
+	_handleLanguage2Local
+};

File diff suppressed because it is too large
+ 126 - 791
uni_modules/z-paging/components/z-paging/js/z-paging-main.js


+ 7 - 19
uni_modules/z-paging/components/z-paging/js/z-paging-mixin.js

@@ -1,36 +1,24 @@
-// z-paging
-// github地址:https://github.com/SmileZXLee/uni-z-paging
-// dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-// 反馈QQ群:790460711
-// 使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法
+// [z-paging]使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法
 
-const ZPagingMixin = {
+export default {
 	onPullDownRefresh() {
-		if (this.isPagingRefNotFound()) {
-			return;
-		}
+		if (this.isPagingRefNotFound()) return;
 		this.$refs.paging.reload();
 	},
 	onPageScroll(e) {
-		if (this.isPagingRefNotFound()) {
-			return;
-		}
+		if (this.isPagingRefNotFound()) return;
 		this.$refs.paging.updatePageScrollTop(e.scrollTop);
 		if (e.scrollTop < 10) {
 			this.$refs.paging.doChatRecordLoadMore();
 		}
 	},
 	onReachBottom() {
-		if (this.isPagingRefNotFound()) {
-			return;
-		}
-		this.$refs.paging.doLoadMore();
+		if (this.isPagingRefNotFound()) return;
+		this.$refs.paging.pageReachBottom();
 	},
 	methods: {
 		isPagingRefNotFound() {
-			return !this.$refs.paging || this.$refs.paging === undefined;
+			return !this.$refs.paging;
 		}
 	}
 }
-
-export default ZPagingMixin;

File diff suppressed because it is too large
+ 1 - 11
uni_modules/z-paging/components/z-paging/js/z-paging-static.js


+ 106 - 78
uni_modules/z-paging/components/z-paging/js/z-paging-utils.js

@@ -1,37 +1,51 @@
-// z-paging
-// github地址:https://github.com/SmileZXLee/uni-z-paging
-// dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-// 反馈QQ群:790460711
-// z-paging工具类
+// [z-paging]工具类
 
-import zI18n from './z-paging-i18n'
+import zConfig from './z-paging-config'
+import zLocalConfig from '../config/index'
 
 const storageKey = 'Z-PAGING-REFRESHER-TIME-STORAGE-KEY'
-
-//判断两个数组是否相等
-function arrayIsEqual(arr1, arr2) {
-	if (arr1 === arr2) {
-		return true;
-	}
-	if (arr1.length !== arr2.length) {
-		return false;
+let config = null;
+
+/*
+当z-paging未使用uni_modules管理时,控制台会有警告:WARNING: Module not found: Error: Can't resolve '@/uni_modules/z-paging'...
+此时注释下方try中的代码即可
+*/
+// #ifdef VUE2
+try {
+	const contextKeys = require.context('@/uni_modules/z-paging', false, /\z-paging-config$/).keys();
+	if (contextKeys.length) {
+		const suffix = '.js';
+		config = require('@/uni_modules/z-paging/z-paging-config' + suffix);
 	}
-	for (let i = 0; i < arr1.length; i++) {
-		if (arr1[i] !== arr2[i]) {
-			return false;
+} catch (e) {}
+// #endif
+
+//获取默认配置信息
+function gc(key, defaultValue) {
+	if (!config) {
+		if (zLocalConfig && Object.keys(zLocalConfig).length) {
+			config = zLocalConfig;
+		} else {
+			const temConfig = zConfig.getConfig();
+			if (zConfig && temConfig) {
+				config = temConfig;
+			}
 		}
 	}
-	return true;
+	if (!config) return defaultValue;
+	const value = config[_toKebab(key)];
+	return value === undefined ? defaultValue : value;
 }
 
+
 //获取最终的touch位置
-function getCommonTouch(e) {
+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 !== {}) {
+	} else if (e.datail && e.datail != {}) {
 		touch = e.datail;
 	} else {
 		return {
@@ -48,25 +62,25 @@ function getCommonTouch(e) {
 //判断当前手势是否在z-paging内触发
 function getTouchFromZPaging(target) {
 	if (target && target.tagName && target.tagName !== 'BODY' && target.tagName !== 'UNI-PAGE-BODY') {
-		var classList = target.classList;
-		if (classList && classList.contains('zp-paging-touch-view')) {
-			return true;
+		const classList = target.classList;
+		if (classList && classList.contains('z-paging-content')) {
+			return {
+				isFromZp: true, 
+				isPageScroll: classList.contains('z-paging-content-page'), 
+				isReachedTop: classList.contains('z-paging-reached-top')
+			};
 		} else {
 			return getTouchFromZPaging(target.parentNode);
 		}
 	} else {
-		return false;
+		return {isFromZp: false};
 	}
 }
 
 //获取z-paging所在的parent
 function getParent(parent) {
-	if (!parent) {
-		return null;
-	}
-	if (parent.$refs.paging) {
-		return parent;
-	}
+	if (!parent) return null;
+	if (parent.$refs.paging) return parent;
 	return getParent(parent.$parent);
 }
 
@@ -75,56 +89,67 @@ function consoleErr(err) {
 	console.error(`[z-paging]${err}`);
 }
 
-//打印警告信息
-function consoleWarn(warn) {
-	console.warn(`[z-paging]${warn}`);
-}
-
 //设置下拉刷新时间
 function setRefesrherTime(time, key) {
-	try {
-		let datas = getRefesrherTime();
-		if (!datas) {
-			datas = {};
-		}
-		datas[key] = time;
-		uni.setStorageSync(storageKey, datas);
-	} catch {}
+	const datas = getRefesrherTime() || {};
+	datas[key] = time;
+	uni.setStorageSync(storageKey, datas);
 }
 
 //获取下拉刷新时间
 function getRefesrherTime() {
-	try {
-		const datas = uni.getStorageSync(storageKey);
-		return datas;
-	} catch {
-		return null;
-	}
+	return uni.getStorageSync(storageKey);
 }
 
 //通过下拉刷新标识key获取下拉刷新时间
 function getRefesrherTimeByKey(key) {
 	const datas = getRefesrherTime();
-	if (datas) {
-		const data = datas[key];
-		if (data) {
-			return data;
-		}
-	}
-	return null;
+	return datas && datas[key] ? datas[key] : null;
 }
 
 //通过下拉刷新标识key获取下拉刷新时间(格式化之后)
-function getRefesrherFormatTimeByKey(key) {
+function getRefesrherFormatTimeByKey(key, textMap) {
 	const time = getRefesrherTimeByKey(key);
-	let timeText = zI18n['refresherUpdateTimeNoneText'][zI18n.getLanguage()];
-	if (time) {
-		timeText = _timeFormat(time);
+	const timeText = time ? _timeFormat(time, textMap) : textMap.none;
+	return `${textMap.title}${timeText}`;
+}
+
+//将文本的px或者rpx转为px的值
+function convertTextToPx(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(uni.upx2px(text));
+		return Number(text);
 	}
-	return `${zI18n['refresherUpdateTimeText'][zI18n.getLanguage()]}${timeText}`;
+	return 0;
+}
+
+//获取当前时间
+function getTime() {
+	return (new Date()).getTime();
+}
+
+//获取z-paging实例id
+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 _timeFormat(time) {
+//------------------ 私有方法 ------------------------
+//时间格式化
+function _timeFormat(time, textMap) {
 	const date = new Date(time);
 	const currentDate = new Date();
 	const dateDay = new Date(time).setHours(0, 0, 0, 0);
@@ -133,47 +158,50 @@ function _timeFormat(time) {
 	let dayStr = '';
 	const timeStr = _dateTimeFormat(date);
 	if (disTime === 0) {
-		dayStr = zI18n['refresherUpdateTimeTodayText'][zI18n.getLanguage()];
+		dayStr = textMap.today;
 	} else if (disTime === -86400000) {
-		dayStr = zI18n['refresherUpdateTimeYesterdayText'][zI18n.getLanguage()];
+		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();
-	if (showYear) {
-		return `${year}-${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}`;
-	} else {
-		return `${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}`;
-	}
+	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();
-	if (str.length === 1) {
-		return '0' + str;
-	}
-	return str;
+	return str.length === 1 ? '0' + str : str;
+}
+
+//驼峰转短横线
+function _toKebab(value) {
+	return value.replace(/([A-Z])/g, "-$1").toLowerCase();
 }
 
-module.exports = {
+export default {
+	gc,
 	setRefesrherTime,
 	getRefesrherFormatTimeByKey,
-	arrayIsEqual,
-	getCommonTouch,
+	getTouch,
 	getTouchFromZPaging,
 	getParent,
-	consoleErr,
-	consoleWarn
+	convertTextToPx,
+	getTime,
+	getInstanceId,
+	consoleErr
 };

+ 29 - 30
uni_modules/z-paging/components/z-paging/wxs/z-paging-renderjs.js

@@ -1,30 +1,26 @@
-// z-paging
-// github地址:https://github.com/SmileZXLee/uni-z-paging
-// dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-// 反馈QQ群:790460711
-// 使用renderjs在app-vue和h5中对touchmove事件冒泡进行处理
+// [z-paging]使用renderjs在app-vue和h5中对touchmove事件冒泡进行处理
+
+import u from '../js/z-paging-utils'
+var data = {
+	startY: 0,
+	isTouchFromZPaging: false,
+	isUsePageScroll: false,
+	isReachedTop: true,
+	isIosAndH5: false
+}
 
-import zUtils from '../js/z-paging-utils'
 export default {
-	data() {
-		return {
-			renderScrollTop: 0,
-			renderUsePageScroll: false,
-			renderIsIos: uni.getSystemInfoSync().platform === 'ios',
-			startY: 0,
-			isTouchFromZPaging: false
-		}
-	},
 	mounted() {
 		this._handleTouch();
+		// #ifdef APP-VUE
+		this.$ownerInstance && this.$ownerInstance.callMethod('_checkVirtualListScroll');
+		// #endif
 	},
 	methods: {
 		//接收逻辑层发送的数据
-		renderPropScrollTopChange(newVal, oldVal, ownerVm, vm) {
-			this.renderScrollTop = newVal;
-		},
-		renderUsePageScrollChange(newVal, oldVal, ownerVm, vm) {
-			this.renderUsePageScroll = newVal;
+		renderPropIsIosAndH5Change(newVal) {
+			if (newVal === -1) return;
+			data.isIosAndH5 = newVal;
 		},
 		//拦截处理touch事件
 		_handleTouch() {
@@ -39,22 +35,25 @@ export default {
 			}
 		},
 		_handleTouchstart(e) {
-			const touch = zUtils.getCommonTouch(e);
-			this.startY = touch.touchY;
-			this.isTouchFromZPaging = zUtils.getTouchFromZPaging(e.target);
+			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;
 		},
 		_handleTouchmove(e) {
-			const touch = zUtils.getCommonTouch(e);
-			var moveY = touch.touchY - this.startY;
-			if ((this.isTouchFromZPaging && this.renderScrollTop < 1 && moveY > 0) || (this.isTouchFromZPaging && this
-					.renderIsIos && !this
-					.renderUsePageScroll && moveY <
-					0)) {
+			const touch = u.getTouch(e);
+			var moveY = touch.touchY - data.startY;
+			if (data.isTouchFromZPaging && ((data.isReachedTop && moveY > 0)  || (data.isIosAndH5 && !data.isUsePageScroll && moveY < 0))) {
 				if (e.cancelable && !e.defaultPrevented) {
 					e.preventDefault();
 				}
 			}
 		},
-
+		_removeAllEventListener(){
+			window.removeEventListener('touchstart');
+			window.removeEventListener('touchmove');
+		}
 	}
 };

+ 167 - 152
uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs

@@ -1,89 +1,102 @@
-// z-paging
-// github地址:https://github.com/SmileZXLee/uni-z-paging
-// dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935
-// 反馈QQ群:790460711
-// 微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
+// [z-paging]微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
 
-var currentMoveDistance = 0;
+var currentDis = 0;
+var isPCFlag = -1;
+var startY = -1;
 
-function propObserver(newValue, oldValue, ownerInstance, instance) {
-	var state = ownerInstance.getState();
-	if (state) {
-		state.currentInstance = instance;
-		var dataset = instance.getDataset();
-		var loading = dataset.loading == true;
-		if (newValue.indexOf('end') != -1) {
-			_setTransform('translateY(0px)', instance)
-			state.moveDistance = 0;
-			state.oldMoveDistance = 0;
-			currentMoveDistance = 0;
-		} else if (newValue.indexOf('begin') != -1) {
-			var refresherThreshold = instance.getDataset().refresherthreshold
-			_setTransformValue(refresherThreshold, instance, state);
-		}
+function propObserver(newValue, oldValue, ownerIns, ins) {
+	var state = ownerIns.getState() || {};
+	state.currentIns = ins;
+	var dataset = ins.getDataset();
+	var loading = dataset.loading == true;
+	if (newValue && newValue.indexOf('end') != -1) {
+		var transition = newValue.split('end')[0];
+		_setTransform('translateY(0px)', ins, false, transition);
+		state.moveDis = 0;
+		state.oldMoveDis = 0;
+		currentDis = 0;
+	} else if (newValue && newValue.indexOf('begin') != -1) {
+		var refresherThreshold = ins.getDataset().refresherthreshold;
+		_setTransformValue(refresherThreshold, ins, state, false);
 	}
 }
 
-function touchstart(e, ownerInstance) {
-	var instance = ownerInstance.getState().currentInstance;
-	var state = instance.getState();
-	var dataset = instance.getDataset();
-	var isTouchEnded = state.isTouchEnded;
-	if (_getRefresherTouchDisabled(e, instance, 0)) {
-		return;
+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;
 	}
-	state.oldMoveDistance = 0;
-	var touch = _getCommonTouch(e);
+	var isTouchEnded = state.isTouchEnded;
+	state.oldMoveDis = 0;
+	var touch = _getTouch(e);
 	var loading = _getIsTrue(dataset.loading);
 	state.startY = touch.touchY;
-	state.lastRefresherTouchmove = touch;
+	startY = state.startY;
+	state.lastTouch = touch;
 	if (!loading && isTouchEnded) {
 		state.isTouchmoving = false;
 	}
 	state.isTouchEnded = false;
-	ownerInstance.callMethod('_handleRefresherTouchstart', touch);
+	ownerIns.callMethod('_handleRefresherTouchstart', touch);
 }
 
-function touchmove(e, ownerInstance) {
-	var touch = _getCommonTouch(e);
-	var instance = ownerInstance.getState().currentInstance;
-	var dataset = instance.getDataset();
+function touchmove(e, ownerIns) {
+	var touch = _getTouch(e);
+	var ins = _getIns(ownerIns);
+	var dataset = ins.getDataset();
 	var refresherThreshold = dataset.refresherthreshold;
-	var state = instance.getState();
-	if (_getRefresherTouchDisabled(e, instance, 1)) {
-		_handleTouchMovePullingDown(state, ownerInstance, false);
+	var isIos = _getIsTrue(dataset.isios);
+	var state = ins.getState();
+	var dataset = ins.getDataset();
+	var watchTouchDirectionChange = _getIsTrue(dataset.watchtouchdirectionchange);
+	var moveDisObj = {};
+	var moveDis = 0;
+	var prevent = false;
+	if (watchTouchDirectionChange) {
+		moveDisObj = _getMoveDis(e, ins);
+		moveDis = moveDisObj.currentDis;
+		prevent = moveDisObj.isDown;
+		if(state.oldAcceptedIsDown == prevent){
+			ownerIns.callMethod('_handleTouchDirectionChange', {direction: prevent ? 'top' : 'bottom'}); 
+			state.oldIsDown = prevent;
+		}
+		state.oldAcceptedIsDown = prevent;
+	}
+	if (_touchDisabled(e, ins, 1)) {
+		_handlePullingDown(state, ownerIns, false);
 		return true;
 	}
 	if (!_getAngleIsInRange(e, touch, state, dataset)) {
-		_handleTouchMovePullingDown(state, ownerInstance, false);
+		_handlePullingDown(state, ownerIns, false);
 		return true;
 	}
-	var moveDistanceObj = _getMoveDistance(e, instance);
-	var moveDistance = moveDistanceObj.currentMoveDistance;
-	var prevent = moveDistanceObj.isDown;
-	if (moveDistance < 0) {
-		_setTransformValue(0, instance, state);
-		_handleTouchMovePullingDown(state, ownerInstance, false);
+	moveDisObj = _getMoveDis(e, ins);
+	moveDis = moveDisObj.currentDis;
+	prevent = moveDisObj.isDown;
+	if (moveDis < 0) {
+		_setTransformValue(0, ins, state, false);
+		_handlePullingDown(state, ownerIns, false);
 		return true;
 	}
 	if (prevent && !state.disabledBounce) {
-		ownerInstance.callMethod('_handleScrollViewDisableBounce', {
-			bounce: false
-		});
+		if (isIos) {
+			ownerIns.callMethod('_handleScrollViewDisableBounce', {bounce: false}); 
+		}
 		state.disabledBounce = true;
-		_handleTouchMovePullingDown(state, ownerInstance, prevent);
+		_handlePullingDown(state, ownerIns, prevent);
 		return !prevent;
 	}
-	_setTransformValue(moveDistance, instance, state);
+	_setTransformValue(moveDis, ins, state, false);
 	var oldRefresherStatus = state.refresherStatus;
-	var dataset = instance.getDataset();
 	var oldIsTouchmoving = _getIsTrue(dataset.oldistouchmoving);
+	var hasTouchmove = _getIsTrue(dataset.hastouchmove);
 	var isTouchmoving = state.isTouchmoving;
-	if (moveDistance >= refresherThreshold) {
-		state.refresherStatus = 1;
-	} else {
-		state.refresherStatus = 0;
-	}
+	state.refresherStatus = moveDis >= refresherThreshold ? 1 : 0;
 	if (!isTouchmoving) {
 		state.isTouchmoving = true;
 		isTouchmoving = true;
@@ -91,61 +104,61 @@ function touchmove(e, ownerInstance) {
 	if (state.isTouchEnded) {
 		state.isTouchEnded = false;
 	}
-	if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving !=
-		isTouchmoving) {
-		ownerInstance.callMethod('_handleRefresherTouchmove', moveDistance, touch);
+	if (hasTouchmove) {
+		ownerIns.callMethod('_handleWxsPullingDown', {moveDis:moveDis, diffDis:moveDisObj.diffDis});
+	}
+	if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving != isTouchmoving) {
+		ownerIns.callMethod('_handleRefresherTouchmove', moveDis, touch);
 	}
-	_handleTouchMovePullingDown(state, ownerInstance, prevent);
+	_handlePullingDown(state, ownerIns, prevent);
 	return !prevent;
 }
 
-function touchend(e, ownerInstance) {
-	var touch = _getCommonTouch(e);
-	var instance = ownerInstance.getState().currentInstance;
-	var dataset = instance.getDataset();
-	var state = instance.getState();
-	if (_getRefresherTouchDisabled(e, instance, 2)) {
-		return;
-	}
-	state.refresherReachMaxAngle = true;
+function touchend(e, ownerIns) {
+	var touch = _getTouch(e);
+	var ins = _getIns(ownerIns);
+	var dataset = ins.getDataset();
+	var state = ins.getState();
+	if (_touchDisabled(e, ins, 2)) return;
+	state.reachMaxAngle = true;
 	state.hitReachMaxAngleCount = 0;
 	state.disabledBounce = false;
 	state.fixedIsTopHitCount = 0;
-	//ownerInstance.callMethod('_handleScrollViewDisableBounce', {bounce:true});
-	var isTouchmoving = state.isTouchmoving;
-	if (!isTouchmoving) {
-		return;
-	}
+	//ownerIns.callMethod('_handleScrollViewDisableBounce', {bounce:true});
+	if (!state.isTouchmoving) return;
 	var oldRefresherStatus = state.refresherStatus;
-	var oldMoveDistance = state.moveDistance;
-	var refresherThreshold = instance.getDataset().refresherthreshold
-	var moveDistance = _getMoveDistance(e, instance).currentMoveDistance;
-	if (!(moveDistance >= refresherThreshold && oldRefresherStatus === 1)) {
+	var oldMoveDis = state.moveDis;
+	var refresherThreshold = ins.getDataset().refresherthreshold
+	var moveDis = _getMoveDis(e, ins).currentDis;
+	if (!(moveDis >= refresherThreshold && oldRefresherStatus === 1)) {
 		state.isTouchmoving = false;
 	}
-	ownerInstance.callMethod('_handleRefresherTouchend', moveDistance);
+	ownerIns.callMethod('_handleRefresherTouchend', moveDis);
 	state.isTouchEnded = true;
-	if (oldMoveDistance < refresherThreshold) {
-		return;
-	}
-	if (moveDistance >= refresherThreshold) {
-		moveDistance = refresherThreshold;
+	if (oldMoveDis < refresherThreshold) return;
+	var animate = false;
+	if (moveDis >= refresherThreshold) {
+		moveDis = refresherThreshold;
+		animate = true;
 	}
-	_setTransformValue(moveDistance, instance, state)
+	_setTransformValue(moveDis, ins, state, animate);
 }
 
 // #ifdef H5
 function isPC() {
+	if (!navigator) return false;
+	if (isPCFlag != -1) return isPCFlag;
 	var userAgentInfo = navigator.userAgent;
 	var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
 	var flag = true;
-	for (var v = 0; v < Agents.length - 1; v++) {
-		if (userAgentInfo.indexOf(Agents[v]) > 0) {
+	for (var i = 0; i < Agents.length; i++) {
+		if (userAgentInfo.indexOf(Agents[i]) > 0) {
 			flag = false;
 			break;
 		}
 	}
-	return flag;
+	isPCFlag = flag;
+	return isPCFlag;
 }
 
 var movable = false;
@@ -175,64 +188,64 @@ function mouseleave(e, ins) {
 // #endif
 
 
-function _setTransformValue(value, instance, state) {
+function _setTransformValue(value, ins, state, animate) {
 	value = value || 0;
-	if (state.moveDistance == value) {
-		return;
-	}
-	state.moveDistance = value;
-	_setTransform('translateY(' + value + 'px)', instance);
+	if (state.moveDis == value) return;
+	state.moveDis = value;
+	_setTransform('translateY(' + value + 'px)', ins, animate, '');
 }
 
-function _setTransform(transform, instance) {
+function _setTransform(transform, ins, animate, transition) {
 	if (transform == 'translateY(0px)') {
 		transform = 'none';
 	}
-	instance.requestAnimationFrame(function() {
-		instance.setStyle({
-			transform: transform,
-			'-webkit-transform': 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 _getMoveDistance(e, instance) {
-	var state = instance.getState();
-	var refresherThreshold = instance.getDataset().refresherthreshold;
-	var refresherOutRate = instance.getDataset().refresheroutrate;
-	refresherThreshold = parseFloat(refresherThreshold);
-	refresherOutRate = parseFloat(refresherOutRate);
-	var touch = _getCommonTouch(e);
-	var moveDistance = touch.touchY - state.startY;
-	var oldMoveDistance = state.oldMoveDistance || 0;
-	state.oldMoveDistance = moveDistance;
-	var diffDis = moveDistance - oldMoveDistance;
+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) {
-		diffDis = diffDis * 0.85;
-		if (currentMoveDistance > refresherThreshold) {
+		diffDis = diffDis * refresherPullRate;
+		if (currentDis > refresherThreshold) {
 			diffDis = diffDis * (1 - refresherOutRate);
 		}
 	}
-	currentMoveDistance += diffDis;
-	if (currentMoveDistance < 0) {
-		currentMoveDistance = 0;
-	}
+	diffDis = diffDis > 100 ? diffDis / 100 : diffDis;
+	currentDis += diffDis;
+	currentDis = Math.max(0, currentDis);
 	return {
-		currentMoveDistance: currentMoveDistance,
+		currentDis: currentDis,
+		diffDis: diffDis,
 		isDown: diffDis > 0
 	};
 }
 
-function _getCommonTouch(e) {
-	var touch = null;
+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 !== {}) {
+	} else if (e.datail && e.datail != {}) {
 		touch = e.datail;
-	} else {
-		touch = e;
 	}
 	return {
 		touchX: touch.clientX,
@@ -240,9 +253,17 @@ function _getCommonTouch(e) {
 	};
 }
 
-function _getRefresherTouchDisabled(e, instance, processTag) {
-	var dataset = instance.getDataset();
-	var state = instance.getState();
+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 = _getIsTrue(dataset.loading);
 	var useChatRecordMode = _getIsTrue(dataset.usechatrecordmode);
 	var refresherEnabled = _getIsTrue(dataset.refresherenabled);
@@ -258,7 +279,7 @@ function _getRefresherTouchDisabled(e, instance, processTag) {
 	}
 	var fixedIsTopHitCount = state.fixedIsTopHitCount || 0;
 	if (fixedIsTop) {
-		fixedIsTopHitCount++;
+		fixedIsTopHitCount ++;
 		if (fixedIsTopHitCount <= 3) {
 			fixedIsTop = false;
 		}
@@ -272,50 +293,44 @@ function _getRefresherTouchDisabled(e, instance, processTag) {
 	if (!isIos && processTag === 2) {
 		fixedIsTop = true;
 	}
-	var res = loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher || ((
-		usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) || ((
-		!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
-	return res;
+	return loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher || 
+	((usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) || 
+	((!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
 }
 
 function _getAngleIsInRange(e, touch, state, dataset) {
-	var refresherMaxAngle = dataset.refreshermaxangle;
+	var maxAngle = dataset.refreshermaxangle;
 	var refresherAecc = _getIsTrue(dataset.refresheraecc);
-	var lastRefresherTouchmove = state.lastRefresherTouchmove;
-	var refresherReachMaxAngle = state.refresherReachMaxAngle;
-	var moveDistance = state.oldMoveDistance;
-	if (!lastRefresherTouchmove) {
-		return true;
-	}
-	if (refresherMaxAngle >= 0 && refresherMaxAngle <= 90 && lastRefresherTouchmove) {
-		if ((!moveDistance || moveDistance < 1) && !refresherAecc && refresherReachMaxAngle != null && !
-			refresherReachMaxAngle) {
-			return false;
-		}
-		var x = Math.abs(touch.touchX - lastRefresherTouchmove.touchX);
-		var y = Math.abs(touch.touchY - lastRefresherTouchmove.touchY);
+	var lastTouch = state.lastTouch;
+	var reachMaxAngle = state.reachMaxAngle;
+	var moveDis = state.oldMoveDis;
+	if (!lastTouch) return true;
+	if (maxAngle >= 0 && maxAngle <= 90 && lastTouch) {
+		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 < refresherMaxAngle) {
+			if (angle < maxAngle) {
 				var hitReachMaxAngleCount = state.hitReachMaxAngleCount || 0;
 				state.hitReachMaxAngleCount = ++hitReachMaxAngleCount;
 				if (state.hitReachMaxAngleCount > 2) {
-					state.lastRefresherTouchmove = touch;
-					state.refresherReachMaxAngle = false;
+					state.lastTouch = touch;
+					state.reachMaxAngle = false;
 				}
 				return false;
 			}
 		}
 	}
-	state.lastRefresherTouchmove = touch;
+	state.lastTouch = touch;
 	return true;
 }
 
-function _handleTouchMovePullingDown(state, instance, onPullingDown) {
+function _handlePullingDown(state, ins, onPullingDown) {
 	var oldOnPullingDown = state.onPullingDown || false;
 	if (oldOnPullingDown != onPullingDown) {
-		instance.callMethod('_handleWxsOnPullingDown', onPullingDown);
+		ins.callMethod('_handleWxsPullingDownStatusChange', onPullingDown);
 	}
 	state.onPullingDown = onPullingDown;
 }

+ 268 - 191
uni_modules/z-paging/components/z-paging/z-paging.vue

@@ -4,235 +4,312 @@
   / /_____| |_) | (_| | (_| | | | | | (_| |
  /___|    | .__/ \__,_|\__, |_|_| |_|\__, |
           |_|          |___/         |___/ 
-V1.9.3
-by ZXLee 2021-07-12
--- >
-<!-- API文档地址:http://z-paging.com -->
+v2.4.8 (2022-11-03)
+by ZXLee
+-->
+<!-- API文档地址:https://z-paging.zxlee.cn -->
 <!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
 <!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
 <!-- 反馈QQ群:790460711 -->
 
 <template name="z-paging">
 	<!-- #ifndef APP-NVUE -->
-	<view :class="!usePageScroll&&fixed?'z-paging-content z-paging-content-fixed':'z-paging-content'"
-		:style="[finalPagingStyle]">
+	<view :class="{'z-paging-content':true,'z-paging-content-fixed':!usePageScroll&&fixed,'z-paging-content-page':usePageScroll,'z-paging-reached-top':renderPropScrollTop<1}" :style="[finalPagingStyle]">
+		<!-- #ifndef APP-PLUS -->
+		<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
+		<!-- #endif -->
 		<!-- 顶部固定的slot -->
-		<slot v-if="!usePageScroll&&$slots.top" name="top"></slot>
-		<view class="zp-page-scroll-top" v-else-if="usePageScroll&&$slots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
-			<slot name="top"></slot>
+		<slot v-if="!usePageScroll&&$slots.top" name="top" />
+		<view class="zp-page-top" v-else-if="usePageScroll&&$slots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
+			<slot name="top" />
 		</view>
-		<view :class="{'zp-scroll-view-super':!usePageScroll}" :style="[finalScrollViewStyle]">
-			<scroll-view
-				:class="{'zp-scroll-view':true,'zp-scroll-view-absolute':!usePageScroll}"
-				:scroll-top="scrollTop"
-				:scroll-y="scrollable&&!usePageScroll&&scrollEnable" :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="refresherTriggered" @scroll="_scroll" @scrolltolower="_onLoadingMore('toBottom')"
-				@scrolltoupper="_scrollToUpper" @refresherrestore="_onRestore" @refresherrefresh="_onRefresh"  
-				>	
-				<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-color': refresherFixedBackground,'height': `${finalRefresherFixedBacHeight}px`}]"></view>
-					<view class="zp-paging-main" :style="[{'transform': finalRefresherTransform,'transition': refresherTransition}]"
-					<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
-					:change:prop="pagingWxs.propObserver" :prop="wxsPropType"
-					:data-refresherThreshold="finalRefresherThreshold" :data-isIos="isIos"
-					:data-loading="loading" :data-useChatRecordMode="useChatRecordMode" 
-					:data-refresherEnabled="refresherEnabled" :data-useCustomRefresher="useCustomRefresher" :data-pageScrollTop="wxsPageScrollTop"
-					:data-scrollTop="wxsScrollTop" :data-refresherMaxAngle="refresherMaxAngle" 
-					:data-refresherAecc="refresherAngleEnableChangeContinued" :data-usePageScroll="usePageScroll"
-					:data-oldIsTouchmoving="isTouchmoving" :data-refresherOutRate="finalRefresherOutRate"
+		<view :class="{'zp-view-super':true,'zp-scroll-view-super':!usePageScroll}" :style="[finalScrollViewStyle]">
+			<view v-if="$slots.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}"
+					:scroll-top="scrollTop" :scroll-x="scrollX"
+					:scroll-y="scrollable&&!usePageScroll&&scrollEnable&&(refresherCompleteScrollable?true:refresherStatus!==R.Complete)" :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="_onLoadingMore('toBottom')"
+					@scrolltoupper="_scrollToUpper" @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 || H5 -->
-					:change:renderPropScrollTop="pagingRenderjs.renderPropScrollTopChange" :renderPropScrollTop="renderPropScrollTop"
-					:change:renderUsePageScroll="pagingRenderjs.renderUsePageScrollChange" :renderUsePageScroll="renderUsePageScroll"
+					<!-- #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="finalRefresherEnabled&&useCustomRefresher&&isTouchmoving" class="zp-custom-refresher-view"
-							:style="[{'margin-top': `-${finalRefresherThreshold}px`,'background-color': refresherBackground}]">
-							<view :style="[{'height': `${finalRefresherThreshold}px`,'background-color': refresherBackground}]">
-								<!-- 下拉刷新view -->
-								<slot 
-								<!-- #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO || MP-BAIDU  -->
-								v-if="zScopedSlots.refresher"
-								<!-- #endif -->
-								<!-- #ifndef MP-WEIXIN || MP-QQ || MP-TOUTIAO || MP-BAIDU -->
-								v-if="$scopedSlots.refresher||$slots.refresher"
-								<!-- #endif -->
-								<!-- #ifndef MP-QQ -->
-								:refresherStatus="refresherStatus"
-								<!-- #endif -->
-								name="refresher" />
-								<z-paging-refresh ref="refresh" v-else :style="[{'height': `${finalRefresherThreshold}px`}]" :refresherStatus="refresherStatus"
-									:defaultThemeStyle="finalRefresherThemeStyle" :refresherDefaultText="finalRefresherDefaultText"
-									:refresherPullingText="finalRefresherPullingText" :refresherRefreshingText="finalRefresherRefreshingText" 
-									:showRefresherUpdateTime="showRefresherUpdateTime" :refresherUpdateTimeKey="refresherUpdateTimeKey"></z-paging-refresh>
-							</view>
-						</view>
-						<view class="zp-paging-container">
-							<slot v-if="useChatRecordMode&&$slots.chatLoading&&loadingStatus!==2&&realTotalData.length"
-								name="chatLoading" />
-							<view v-else-if="useChatRecordMode&&loadingStatus!==2&&realTotalData.length"
-								class="zp-chat-record-loading-container">
-								<text v-if="loadingStatus!==1" @click="_scrollToUpper()"
-									:class="defaultThemeStyle==='white'?'zp-loading-more-text zp-loading-more-text-white':'zp-loading-more-text zp-loading-more-text-black'">{{chatRecordLoadingMoreText}}</text>
-								<image v-else :src="base64Flower" class="zp-chat-record-loading-custom-image">
-								</image>
+						<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-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-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}px`,'background': refresherBackground,'opacity': isTouchmoving ? 1 : 0}]">
+								<view class="zp-custom-refresher-container" :style="[{'height': `${finalRefresherThreshold}px`,'background': refresherBackground}]">
+									<!-- 下拉刷新view -->
+									<view class="zp-custom-refresher-slot-view">
+										<slot v-if="!($slots.refresherComplete&&refresherStatus===R.Complete)" :refresherStatus="refresherStatus" name="refresher" />
+									</view>
+									<slot v-if="$slots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+									<z-paging-refresh ref="refresh" v-else-if="!showCustomRefresher" :style="[{'height': `${finalRefresherThreshold}px`}]" :status="refresherStatus"
+										:defaultThemeStyle="finalRefresherThemeStyle" :defaultText="finalRefresherDefaultText"
+										:pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText"
+										:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg"
+										:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+										:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" />
+								</view>
 							</view>
-							<slot v-if="$slots.loading&&!firstPageLoaded&&(autoHideLoadingAfterFirstLoaded?!pagingLoaded:true)&&loading" name="loading" />
-							<!-- 空数据图 -->
-							<view class="zp-empty-view"
-								v-if="showEmpty">
-								<slot v-if="$slots.empty" name="empty" />
-								<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" @reload="_emptyViewReload">
-								</z-paging-empty-view>
+							<view class="zp-paging-container">
+								<slot v-if="useChatRecordMode&&$slots.chatLoading&&loadingStatus!==M.NoMore&&realTotalData.length" name="chatLoading" />
+								<view v-else-if="useChatRecordMode&&loadingStatus!==M.NoMore&&realTotalData.length" class="zp-chat-record-loading-container">
+									<text v-if="loadingStatus!==M.Loading" @click="_scrollToUpper()"
+										:class="defaultThemeStyle==='white'?'zp-loading-more-text zp-loading-more-text-white':'zp-loading-more-text zp-loading-more-text-black'">{{chatRecordLoadingMoreText}}</text>
+									<image v-else :src="base64Flower" class="zp-chat-record-loading-custom-image" />
+								</view>
+								<!-- 全屏Loading -->
+								<slot v-if="showLoading&&$slots.loading&&!loadingFullFixed" name="loading" />
+								<!-- 主体内容 -->
+								<view class="zp-paging-container-content" :style="[{transform:virtualPlaceholderTopHeight>0?`translateY(${virtualPlaceholderTopHeight}px)`:'none'},finalPagingContentStyle]">
+									<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="`zp-id-${item['zp_index']}`" v-for="(item,index) in virtualList" :key="item['zp_unique_index']" @click="_innerCellClick(item,virtualTopRangeIndex+index)">
+													<view v-if="useCompatibilityMode">使用兼容模式请在组件源码z-paging.vue第99行中注释这一行,并打开下面一行注释</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>
+									<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" :config="zLoadMoreConfig" />
+									<!-- #endif -->
+									<!-- #ifdef MP-ALIPAY -->
+									<slot v-if="loadingStatus===M.Default&&$slots.loadingMoreDefault&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreDefault" />
+									<slot v-else-if="loadingStatus===M.Loading&&$slots.loadingMoreLoading&&showLoadingMore&&loadingMoreEnabled" name="loadingMoreLoading" />
+									<slot v-else-if="loadingStatus===M.NoMore&&$slots.loadingMoreNoMore&&showLoadingMore&&showLoadingMoreNoMoreView&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreNoMore" />
+									<slot v-else-if="loadingStatus===M.Fail&&$slots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreFail" />
+									<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===M.NoMore&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :config="zLoadMoreConfig" />
+									<!-- #endif -->
+									<view v-if="safeAreaInsetBottom && useSafeAreaPlaceholder" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+								</view>
+								<!-- 空数据图 -->
+								<view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}" :style="[{emptyViewSuperStyle}]" v-if="showEmpty">
+									<slot v-if="$slots.empty" name="empty" />
+									<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" 
+									@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
+								</view>
 							</view>
-							<!-- 主体内容 -->
-							<view class="zp-paging-container-content" :style="[finalPagingContentStyle]">
-								<slot />
-							</view>
-							<!-- 上拉加载更多view -->
-							<!-- #ifndef MP-ALIPAY -->
-							<slot v-if="_shouldShowLoading('loadingMoreDefault')" name="loadingMoreDefault" />
-							<slot v-else-if="_shouldShowLoading('loadingMoreLoading')" name="loadingMoreLoading" />
-							<slot v-else-if="_shouldShowLoading('loadingMoreNoMore')" name="loadingMoreNoMore" />
-							<slot v-else-if="_shouldShowLoading('loadingMoreFail')" name="loadingMoreFail" />
-							<z-paging-load-more @click.native="_onLoadingMore('click')"
-								v-else-if="_shouldShowLoading('loadingMoreCustom')" :zConfig="zPagingLoadMoreConfig">
-							</z-paging-load-more>
-							<!-- #endif -->
-							<!-- #ifdef MP-ALIPAY -->
-							<slot v-if="loadingStatus===0&&$slots.loadingMoreDefault&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode"
-								name="loadingMoreDefault" />
-							<slot v-else-if="loadingStatus===1&&$slots.loadingMoreLoading&&showLoadingMore&&loadingMoreEnabled"
-								name="loadingMoreLoading" />
-							<slot v-else-if="loadingStatus===2&&$slots.loadingMoreNoMore&&showLoadingMore&&showLoadingMoreNoMoreView&&loadingMoreEnabled&&!useChatRecordMode"
-								name="loadingMoreNoMore" />
-							<slot v-else-if="loadingStatus===3&&$slots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode"
-								name="loadingMoreFail" />
-							<z-paging-load-more @click.native="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===2&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :zConfig="zPagingLoadMoreConfig">
-							</z-paging-load-more>
-							<!-- #endif -->
 						</view>
 					</view>
-				</view>
-			</scroll-view>
+				</scroll-view>
+			</view>
+			<view v-if="$slots.right" :class="{'zp-page-right':true,'zp-absoulte zp-right':finalIsOldWebView}">
+				<slot name="right" />
+			</view>
 		</view>
-		<slot v-if="!usePageScroll&&$slots.bottom" name="bottom"></slot>
-		<view class="zp-page-scroll-bottom" v-else-if="usePageScroll&&$slots.bottom" :style="[{'bottom': `${windowBottom}px`}]">
-			<slot name="bottom"></slot>
+		<!-- 底部固定的slot -->
+		<slot v-if="!usePageScroll&&$slots.bottom" name="bottom" />
+		<view class="zp-page-bottom" v-else-if="usePageScroll&&$slots.bottom" :style="[{'bottom': `${windowBottom}px`}]">
+			<slot name="bottom" />
 		</view>
+		<!-- 点击返回顶部view -->
 		<view v-if="showBackToTopClass" :class="backToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
-			<image class="zp-back-to-top-img" :src="backToTopImg.length?backToTopImg:base64BackToTop"></image>
-		</view>  
+			<slot v-if="$slots.backToTop" name="backToTop" />
+			<image v-else class="zp-back-to-top-img" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
+		</view>
+		<!-- 全屏Loading(铺满z-paging并固定) -->
+		<view v-if="showLoading&&$slots.loading&&loadingFullFixed" class="zp-loading-fixed">
+			<slot name="loading" />
+		</view>
 	</view>
 	<!-- #endif -->
 	<!-- #ifdef APP-NVUE -->
-	<view :is="finalNvueSuperListIs">
-		<view ref="zp-page-scroll-top" :is="nViewIs" class="zp-page-scroll-top" v-if="$slots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
-			<slot name="top"></slot>
+	<component :is="finalNvueSuperListIs" :style="[finalPagingStyle]" :class="{'z-paging-content-fixed':fixed&&!usePageScroll}" :scrollable="false">
+		<!-- 顶部固定的slot -->
+		<view ref="zp-page-top" v-if="$slots.top" :class="{'zp-page-top':usePageScroll}" :style="[usePageScroll?{'top':`${windowTop}px`,'z-index':topZIndex}:{}]">
+			<slot name="top" />
 		</view>
-		<view ref="n-list" id="z-paging-nlist" :style="[scrollViewStyle,useChatRecordMode ? {transform: nIsFirstPageAndNoMore?'rotate(0deg)':'rotate(180deg)'}:{}]" :is="finalNvueListIs" alwaysScrollableVertical="true"
-			:fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar" :loadmoreoffset="finalLowerThreshold"
-			:scrollable="scrollable&&scrollEnable" :bounce="nvueBounce" :column-count="nWaterfallColumnCount" :column-width="nWaterfallColumnWidth"
-			:column-gap="nWaterfallColumnGap" :left-gap="nWaterfallLeftGap" :right-gap="nWaterfallRightGap"
-			@loadmore="_nOnLoadmore" @scroll="_nOnScroll">
-			<refresh class="zp-n-refresh" v-if="finalNvueListIs!=='view'&&refresherEnabled&&!nShowRefresherReveal&&!useChatRecordMode" :display="nRefresherLoading?'show':'hide'" @refresh="_nOnRrefresh"
-				@pullingdown="_nOnPullingdown">
-				<view ref="zp-n-refresh-container" class="zp-n-refresh-container">
-					<!-- 下拉刷新view -->
-					<slot v-if="zScopedSlots.refresher" :refresherStatus="refresherStatus" name="refresher" />
-					<z-paging-refresh ref="refresh" v-else :refresherStatus="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle"
-						:refresherDefaultText="finalRefresherDefaultText" :refresherPullingText="finalRefresherPullingText" :refresherRefreshingText="finalRefresherRefreshingText" 
-						:showRefresherUpdateTime="showRefresherUpdateTime" :refresherUpdateTimeKey="refresherUpdateTimeKey"></z-paging-refresh>
-				</view>
-			</refresh>
-			<view ref="zp-n-list-top-tag" class="zp-n-list-top-tag" :is="nViewIs"></view>
-			<view v-if="nShowRefresherReveal" ref="zp-n-list-refresher-reveal" :style="[{transform:`translateY(-${nShowRefresherRevealHeight}px)`,height:'0px'}]" :is="nViewIs">
-				<slot v-if="zScopedSlots.refresher" :refresherStatus="refresherStatus" name="refresher" />
-				<z-paging-refresh ref="refresh" v-else :refresherStatus="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle"
-					:refresherDefaultText="finalRefresherDefaultText" :refresherPullingText="finalRefresherPullingText" :refresherRefreshingText="finalRefresherRefreshingText" 
-					:showRefresherUpdateTime="showRefresherUpdateTime" :refresherUpdateTimeKey="refresherUpdateTimeKey"></z-paging-refresh>
+		<component :is="finalNvueSuperListIs" class="zp-n-list-container" :scrollable="false">
+			<view v-if="$slots.left" class="zp-page-left">
+				<slot name="left" />
 			</view>
-			<slot />
-			<!-- 空数据图 -->
-			<view style="flex: 1;" key="z-paging-empty-view" :style="[scrollViewStyle,useChatRecordMode ? {transform: nIsFirstPageAndNoMore?'rotate(0deg)':'rotate(180deg)'}:{}]" v-if="showEmpty" :is="finalNvueListIs==='scroller'?'view':finalNvueListIs==='waterfall'?'header':'cell'">
-				<view class="zp-empty-view">
-					<slot v-if="$slots.empty" name="empty" />
-					<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" @reload="_emptyViewReload">
-					</z-paging-empty-view>
-				</view>
-			</view>
-			<view ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag" is="header"></view>
-			<!-- 全屏 -->
-			<view style="flex: 1;" :style="[scrollViewStyle,useChatRecordMode ? {transform: nIsFirstPageAndNoMore?'rotate(0deg)':'rotate(180deg)'}:{}]" v-if="$slots.loading&&!firstPageLoaded&&(autoHideLoadingAfterFirstLoaded?!pagingLoaded:true)&&loading" :is="finalNvueListIs==='scroller'?'view':finalNvueListIs==='waterfall'?'header':'cell'">
-				<slot name="loading" />
-			</view>
-			<!-- 上拉加载更多view -->
-			<view :is="nViewIs">
-				<view v-if="useChatRecordMode">
-					<view v-if="loadingStatus!==2&&realTotalData.length">
-						<slot v-if="$slots.chatLoading"
-							name="chatLoading" />
-						<view v-else class="zp-chat-record-loading-container">
-							<text v-if="loadingStatus!==1" @click="_scrollToUpper()"
-								:class="defaultThemeStyle==='white'?'zp-loading-more-text zp-loading-more-text-white':'zp-loading-more-text zp-loading-more-text-black'">{{chatRecordLoadingMoreText}}</text>
-							<view>
-								<loading-indicator v-if="loadingStatus===1" :animating="true"
-									class="zp-loading-more-line-loading-image">
-								</loading-indicator>
+			<component :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},nChatRecordRotateStyle]" :alwaysScrollableVertical="true"
+				:fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar&&!useChatRecordMode" :loadmoreoffset="finalLowerThreshold" :enable-back-to-top="enableBackToTop"
+				:scrollable="scrollable&&scrollEnable&&(refresherCompleteScrollable?true:refresherStatus!==R.Complete)" :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">
+				<refresh v-if="($slots.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 -->
+						<slot v-if="$slots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+						<slot v-else-if="(nScopedSlots?nScopedSlots:$slots).refresher" :refresherStatus="refresherStatus" name="refresher" />
+						<z-paging-refresh ref="refresh" v-else :status="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle"
+							:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText"
+							:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg"
+							:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+							:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" />
+					</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 -->
+					<slot v-if="$slots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+					<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"
+						:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" 
+						:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg"
+						:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+						:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" />
+				</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&&$slots.loading&&!loadingFullFixed" :class="{'z-paging-content-fixed':usePageScroll}" style="flex: 1;" :style="[nChatRecordRotateStyle]">
+					<slot name="loading" />
+				</component>
+				<!-- 空数据图 -->
+				<component :is="nViewIs" v-if="showEmpty" :class="{'z-paging-content-fixed':usePageScroll}" :style="[{flex:emptyViewCenter?1:0},emptyViewSuperStyle,nChatRecordRotateStyle]">
+					<view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}">
+						<slot v-if="$slots.empty" name="empty" />
+						<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" 
+						@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
+					</view>
+				</component>
+				<component is="header" v-if="!hideNvueBottomTag" ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag"></component>
+				<!-- 上拉加载更多view -->
+				<component :is="nViewIs" v-if="!refresherOnly&&loadingMoreEnabled">
+					<view v-if="useChatRecordMode">
+						<view v-if="loadingStatus!==M.NoMore&&realTotalData.length">
+							<slot v-if="$slots.chatLoading" name="chatLoading" />
+							<view v-else class="zp-chat-record-loading-container">
+								<text v-if="loadingStatus!==M.Loading" @click="_scrollToUpper()"
+									:class="defaultThemeStyle==='white'?'zp-loading-more-text zp-loading-more-text-white':'zp-loading-more-text zp-loading-more-text-black'">{{chatRecordLoadingMoreText}}</text>
+								<view>
+									<loading-indicator v-if="loadingStatus===M.Loading" class="zp-line-loading-image" animating />
+								</view>
 							</view>
 						</view>
 					</view>
-				</view>
-				<view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:'80rpx'}:{}">
-					<slot v-if="_shouldShowLoading('loadingMoreDefault')" name="loadingMoreDefault" />
-					<slot v-else-if="_shouldShowLoading('loadingMoreLoading')" name="loadingMoreLoading" />
-					<slot v-else-if="_shouldShowLoading('loadingMoreNoMore')" name="loadingMoreNoMore" />
-					<slot v-else-if="_shouldShowLoading('loadingMoreFail')" name="loadingMoreFail" />
-					<z-paging-load-more @click.native="_onLoadingMore('click')"
-						v-else-if="_shouldShowLoading('loadingMoreCustom')" :zConfig="zPagingLoadMoreConfig">
-					</z-paging-load-more>
-				</view>
+					<view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:'80rpx'}:{}">
+						<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" :config="zLoadMoreConfig" />
+						<view v-if="safeAreaInsetBottom && useSafeAreaPlaceholder" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+					</view>
+				</component>
+			</component>
+			<view v-if="$slots.right" class="zp-page-right">
+				<slot name="right" />
 			</view>
-		</view>
-		<slot name="bottom"></slot>
+		</component>
+		<!-- 底部固定的slot -->
+		<slot name="bottom" />
+		<!-- 点击返回顶部view -->
 		<view v-if="showBackToTopClass" :class="backToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
-			<image class="zp-back-to-top-img" :src="backToTopImg.length?backToTopImg:base64BackToTop"></image>
+			<slot v-if="$slots.backToTop" name="backToTop" />
+			<image v-else class="zp-back-to-top-img" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
 		</view>
-	</view>
+		<!-- 全屏Loading(铺满z-paging并固定) -->
+		<view v-if="showLoading&&$slots.loading&&loadingFullFixed" class="zp-loading-fixed">
+			<slot name="loading" />
+		</view>
+	</component>
 	<!-- #endif -->
 </template>
-
-<script
-    src="./js/z-paging-main.js"></script>
 <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
-<script
-    src="./wxs/z-paging-wxs.wxs"
-    module="pagingWxs"
-    lang="wxs"
-></script>
+<script src="./wxs/z-paging-wxs.wxs" module="pagingWxs" lang="wxs"></script>
 <!-- #endif -->
-
-<!-- #ifdef APP-VUE || H5 -->
 <script module="pagingRenderjs" lang="renderjs">
 	import pagingRenderjs from './wxs/z-paging-renderjs.js';
+	/**
+	 * z-paging 分页组件
+	 * @description 高性能,全平台兼容。支持虚拟列表,支持nvue、vue3
+	 * @tutorial https://z-paging.zxlee.cn
+	 * @notice 以下仅为部分常用属性、方法和事件,完整文档请查阅z-paging官网
+	 * @property {Number|String} default-page-no 自定义初始的pageNo,默认为1
+	 * @property {Number|String} default-page-size 自定义pageSize,默认为10
+	 * @property {Object} paging-style 设置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} bg-color z-paging的背景色,优先级低于pagingStyle中设置的background。传字符串,如"#ffffff"
+	 * @property {Boolean} use-page-scroll 使用页面滚动,默认为否
+	 * @property {Boolean} use-virtual-list 是否使用虚拟列表,默认为否
+	 * @property {Boolean} fixed z-paging是否使用fixed布局,若使用fixed布局,则z-paging的父view无需固定高度,z-paging高度默认为100%,默认为是(当使用内置scroll-view滚动时有效)
+	 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区域适配,默认为否
+	 * @property {Boolean} auto [z-paging]mounted后是否自动调用reload方法(mounted后自动调用接口),默认为是
+	 * @property {Boolean} show-refresher-when-reload 列表刷新时是否自动显示下拉刷新view,默认为否
+	 * @property {Boolean} loading-more-enabled 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为是
+	 * @property {String|Object} empty-view-text 空数据图描述文字,默认为“没有数据哦~”
+	 * @property {String} empty-view-img 空数据图图片,默认使用z-paging内置的图片
+	 * @property {Boolean} auto-show-back-to-top 自动显示点击返回顶部按钮,默认为否
+	 * @property {Boolean} show-refresher-update-time 是否显示上次下拉刷新更新时间,默认为否
+	 * @property {Boolean} use-chat-record-mode 使用聊天记录模式,默认为否
+	 * @property {String} nvue-list-is nvue中修改列表类型,可选值有list、waterfall和scroller,默认为list
+	 * @event {Function} query 下拉刷新或滚动到底部时会自动触发此方法。z-paging加载时也会触发(若要禁止,请设置:auto="false")。pageNo和pageSize会自动计算好,直接传给服务器即可。
+	 * @example <z-paging ref="paging" v-model="dataList" @query="queryList"></z-paging>
+	 */
 	export default {
-		mixins: [pagingRenderjs]
+		name:"z-paging",
+		// #ifdef APP-VUE || H5
+		mixins: [pagingRenderjs],
+		// #endif
 	}
 </script>
-<!-- #endif -->
-
+<script src="./js/z-paging-main.js" />
+	
 <style scoped>
 	@import "./css/z-paging-main.css";
 	@import "./css/z-paging-static.css";

+ 21 - 15
uni_modules/z-paging/package.json

@@ -1,24 +1,21 @@
 {
   "id": "z-paging",
-  "displayName": "超简单、低耦合!仅需两步轻松完成完整分页逻辑(下拉刷新、上拉加载更多)",
-  "version": "1.9.3",
-  "description": "【支持nvue,使用wxs+renderjs实现】全平台兼容,支持自定义下拉刷新、上拉加载更多,支持自动管理空数据图、点击返回顶部,支持聊天分页、本地分页,支持展示最后更新时间,支持国际化等等",
+  "name": "z-paging",
+  "displayName": "【z-paging下拉刷新、上拉加载】高性能,全平台兼容。支持虚拟列表,支持nvue、vue3",
+  "version": "2.4.8",
+  "description": "超简单、低耦合!使用wxs+renderjs实现。支持长列表优化,支持自定义下拉刷新、上拉加载更多,支持自动管理空数据图、点击返回顶部,支持聊天分页、本地分页,支持国际化等100+项配置",
   "keywords": [
+    "下拉刷新",
+    "上拉加载",
     "分页器",
     "nvue",
-    "wxs",
-    "自定义下拉刷新",
-    "自定义上拉加载"
+    "虚拟列表"
 ],
   "repository": "https://github.com/SmileZXLee/uni-z-paging",
   "engines": {
     "HBuilderX": "^3.0.7"
   },
-  "dcloudext": {
-    "category": [
-        "前端组件",
-        "通用组件"
-    ],
+"dcloudext": {
     "sale": {
       "regular": {
         "price": "0.00"
@@ -35,7 +32,8 @@
       "data": "无",
       "permissions": "无"
     },
-    "npmurl": ""
+    "npmurl": "https://www.npmjs.com/package/z-paging",
+    "type": "component-vue"
   },
   "uni_modules": {
     "dependencies": [],
@@ -68,11 +66,19 @@
           "阿里": "y",
           "百度": "y",
           "字节跳动": "y",
-          "QQ": "y"
+          "QQ": "y",
+		  "钉钉": "y",
+		  "快手": "y",
+		  "飞书": "y",
+		  "京东": "y"
         },
         "快应用": {
-          "华为": "u",
-          "联盟": "u"
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
         }
       }
     }

+ 13 - 206
uni_modules/z-paging/readme.md

@@ -1,25 +1,23 @@
 # z-paging
 
-> 【uni-app自动分页器】超简单,低耦合!仅需两步轻松完成完整分页逻辑(下拉刷新、上拉加载更多),分页全自动处理。支持自定义加载更多的文字或整个view,自定义下拉刷新样式,自动管理空数据view,支持吸顶效果,支持国际化等。
+[![version](https://img.shields.io/badge/version-2.4.8-blue)](https://github.com/SmileZXLee/uni-z-paging)
+[![license](https://img.shields.io/github/license/SmileZXLee/uni-z-paging)](https://en.wikipedia.org/wiki/MIT_License)
 
-### API文档地址:[http://z-paging.com](http://z-paging.com)
-### 备用API文档地址:[https://www.kancloud.cn/zxlee/z-paging/content](https://www.kancloud.cn/zxlee/z-paging/content) 
+### API文档地址:[https://z-paging.zxlee.cn](https://z-paging.zxlee.cn)
 
+***  
 ### 功能&特点
-
 * 【配置简单】仅需两步(绑定网络请求方法、绑定分页结果数组)轻松完成完整下拉刷新,上拉加载更多功能。
 * 【低耦合,低侵入】分页自动管理。在page中无需处理任何分页相关逻辑,无需在data中定义任何分页相关变量,全由z-paging内部处理。
 * 【超灵活,支持各种类型自定义】支持自定义下拉刷新,自定义上拉加载更多,自带自定义下拉刷新效果,及其他数十种自定义属性。
 * 【功能丰富】支持国际化,支持自定义且自动管理空数据图,支持主题模式切换,支持本地分页,支持聊天分页模式,支持展示最后更新时间,支持吸顶效果,支持内部scroll-view滚动与页面滚动,支持一键滚动到顶部等诸多功能。
-* 【多平台兼容,细致,流畅】支持nvue,支持h5、app及各家小程序;在app-vue、h5、微信小程序、QQ小程序上使用wxs实现下拉刷新,大幅提升性能。多处细节优化,给您精致流畅的体验。
+* 【全平台兼容】支持nvue,vue3,支持h5、app及各家小程序。
+* 【高性能】在app-vue、h5、微信小程序、QQ小程序上使用wxs+renderjs从视图层实现下拉刷新;支持虚拟列表,轻松渲染万级数据!
 
+*** 
 ### 反馈qq群(点击加群):[790460711](https://jq.qq.com/?_wv=1027&k=vU2fKZZH)
-
-<p style="font-size:25px;color:red;font-weight:bold">【注意】由V1.9.0起,fixed属性默认值为true,z-paging默认会铺满屏幕。老项目更新请注意,使用侧滑滚动切换选项卡或需要局部使用z-paging请设置:fixed="false"。如果您希望fixed属性默认为false,请参考文档:z-paging.com,将fixed默认值设置为false。</p>
-
-#### 关于自动引入组件
-
-> `z-paging` 支持[easycom组件规范](https://uniapp.dcloud.io/component/README?id=easycom组件规范),无需引用和注册组件即可直接使用,在正在运行的项目中导入`z-paging`可能会提示:`Unknown custom element:<z-paging> - did you register the component corrently?... `,此时需要重新运行项目即可。
+ 
+*** 
 
 ### 预览
 
@@ -27,207 +25,16 @@
 
 |                 自定义下拉刷新效果+分页演示                  |                      吸顶效果+分页演示                       |
 | :----------------------------------------------------------: | :----------------------------------------------------------: |
-| ![](http://www.zxlee.cn/github/uni-z-paging/uni-z-paging.gif) | ![](http://www.zxlee.cn/github/uni-z-paging/uni-z-paging2.gif) |
+| ![](https://z-paging.zxlee.cn/public/img/uni-z-paging.gif) | ![](https://z-paging.zxlee.cn/public/img/uni-z-paging2.gif) |
 
 |                   滑动切换选项卡+分页演示                    |                    聊天记录模式+分页演示                     |
 | :----------------------------------------------------------: | :----------------------------------------------------------: |
-| ![](http://www.zxlee.cn/github/uni-z-paging/z-paging-demo3.gif) | ![](http://www.zxlee.cn/github/uni-z-paging/z-paging-demo4.gif) |
+| ![](https://z-paging.zxlee.cn/public/img/z-paging-demo3.gif) | ![](https://z-paging.zxlee.cn/public/img/z-paging-demo4.gif) |
 
 ### 在线demo体验地址:
 
-* [http://www.zxlee.cn/github/uni-z-paging/demo/index.html](http://www.zxlee.cn/github/uni-z-paging/demo/index.html)
+* [https://demo.z-paging.zxlee.cn](https://demo.z-paging.zxlee.cn)
 
 | 扫码体验                                                     |
 | ------------------------------------------------------------ |
-| ![](http://www.zxlee.cn/github/uni-z-paging/z-paging-demo.png) |
-
-### 此组件已支持`uni_modules`,下载完整示例时组件在`uni_modules`目录下。
-
-## 基本使用
-
-* ①在`<template>` 中使用@query绑定js中分页请求的方法(`z-paging`会将计算好的pageNo和pageSize两个参数传递到此方法中),然后通过` v-model`绑定列表for循环的list。
-* ②在请求结果回调中,通过调用`z-paging`的`complete()`方法,将请求返回的数组传递给`z-paging`处理,如:`this.$refs.paging.complete(服务器返回的数组);`;若请求失败,调用:`this.$refs.paging.complete(false);`即可。
-* 当tab切换或搜索时,可以通过`this.$refs.paging.reload()`刷新整个列表。
-
-```html
-<template>
-    <view class="content">
-        <z-paging ref="paging" v-model="dataList" @query="queryList">
-            <!-- list数据,建议像下方这样在item外层套一个view,而非直接for循环item,因为slot插入有数量限制 -->
-            <view>
-                <view class="item" v-for="(item,index) in dataList">
-                    <view class="item-title">{{item.title}}</view>
-                </view>
-            </view>
-        </z-paging>
-    </view>
-</template>
-
-<script>
-    export default {
-        data() {
-            return {
-                dataList: [],
-            };
-        },
-        methods: {
-            queryList(pageNo, pageSize) {
-              	//这里的pageNo和pageSize会自动计算好,直接传给服务器即可
-              	//这里的请求只是演示,请替换成自己的项目的网络请求,请在网络请求回调中
-              	//通过this.$refs.paging.complete(请求回来的数组);将请求结果传给z-paging
-                this.$request.queryList(pageNo, pageSize, (data) => {
-                    this.$refs.paging.complete(data);
-                });
-            },
-        },
-    };
-</script>
-
-<style scoped>
-    
-</style>
-```
-
-## 设置自定义emptyView组件示例
-
-* 设置自定义emptyView组件,非必须。空数据时会自动展示空数据组件,不需要自己处理
-
-```html
-<z-paging ref="paging" v-model="dataList" @query="queryList">
-    <!-- 设置自己的emptyView组件,非必须。空数据时会自动展示空数据组件,不需要自己处理 -->
-    <empty-view slot="empty"></empty-view>
-    <view>
-        <view class="item" v-for="(item,index) in dataList">
-            <view class="item-title">{{item.title}}</view>
-        </view>
-    </view>
-</z-paging>
-```
-
-## 自定义加载更多各个状态的描述文字示例
-
-* 以修改【没有更多了】状态描述文字为例(将默认的"没有更多了"修改为"我也是有底线的!")
-
-```html
-<z-paging ref="paging" v-model="dataList" loading-more-no-more-text="我也是有底线的!" @query="queryList">
-    <!-- 设置自己的emptyView组件,非必须。空数据时会自动展示空数据组件,不需要自己处理 -->
-    <view>
-        <view class="item" v-for="(item,index) in dataList">
-            <view class="item-title">{{item.title}}</view>
-        </view>
-    </view>
-</z-paging>
-```
-
-## 自定义下拉刷新view示例
-
-* `use-custom-refresher`需要设置为true(默认为true),此时将不会使用uni自带的下拉刷新,转为使用z-paging自定义的下拉刷新,通过slot可以插入开发者自定义的下拉刷新view。
-
-```html
-<z-paging ref="paging" v-model="dataList" :refresher-threshold="80" @query="queryList">
-  <!-- 自定义下拉刷新view -->
-  <!-- 注意注意注意!!QQ小程序或字节跳动小程序中自定义下拉刷新不支持slot-scope,将导致custom-refresher无法显示 -->
-	<!-- 如果是QQ小程序或字节跳动小程序,请参照demo中的sticky-demo.vue中的写法,此处使用slot-scope是为了减少data中无关变量声明,降低依赖 -->
-	<custom-refresher slot="refresher" slot-scope="{refresherStatus}" :status="refresherStatus"></custom-refresher>
-  <!-- list数据,建议像下方这样在item外层套一个view,而非直接for循环item,因为slot插入有数量限制 -->
-  <view>
-    <view class="item" v-for="(item,index) in dataList" @click="itemClick(item)">
-      <view class="item-title">{{item.title}}</view>
-      <view class="item-detail">{{item.detail}}</view>
-      <view class="item-line"></view>
-    </view>
-  </view>
-</z-paging>
-```
-
-## 自定义加载更多各个状态的描述view示例
-
-* 以修改【没有更多了】状态描述view为例
-
-```html
-<z-paging ref="paging" v-model="dataList" @query="queryList">
-    <view>
-        <view class="item" v-for="(item,index) in dataList">
-            <view class="item-title">{{item.title}}</view>
-        </view>
-    </view>
-    <view style="background-color: red" slot="loadingMoreNoMore">这是完全自定义的没有更多数据view</view>
-</z-paging>
-```
-
-## 使用页面滚动示例
-
-```html
-<!-- 使用页面滚动示例(无需设置z-paging的高度) -->
-<template>
-	<view class="content">
-		<!-- 此时使用了页面的滚动,z-paging不需要有确定的高度,use-page-scroll需要设置为true -->
-		<!-- 注意注意!!这里的ref必须设置且必须等于"paging",否则mixin方法无效 -->
-		<z-paging ref="paging" v-model="dataList" use-page-scroll @query="queryList">
-			<!-- 如果希望其他view跟着页面滚动,可以放在z-paging标签内 -->
-			<!-- list数据,建议像下方这样在item外层套一个view,而非直接for循环item,因为slot插入有数量限制 -->
-			<view>
-				<view class="item" v-for="(item,index) in dataList" :key="index" @click="itemClick(item)">
-					<view class="item-title">{{item.title}}</view>
-					<view class="item-detail">{{item.detail}}</view>
-					<view class="item-line"></view>
-				</view>
-			</view>
-		</z-paging>
-	</view>
-</template>
-
-<script>
-	//使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法(如果全局引入了,就不要这一步,全局引入示例见main.js)
-	import ZPagingMixin from '@/uni_modules/z-paging/components/z-paging/js/z-paging-mixin'
-	export default {
-		//注意这一步不要漏掉,必须注册mixins(如果全局引入了,就不要这一步,全局引入示例见main.js)
-		mixins: [ZPagingMixin],
-		data() {
-			//参见demo
-		},
-		methods: {
-			//参见demo
-		}
-	}
-</script>
-```
-
-## i18n示例
-
-* 请参照demo:`i18n-demo.vue`
-
-## 性能与建议
-
-|            |                   使用内置scroll-view滚动                    |                         使用页面滚动                         |                           使用nvue                           |
-| :--------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
-|  **说明**  | 默认模式,`z-paging`需要有确定的高度,下拉刷新与上拉加载更多由`z-paging`内部处理,配置简单。 | `use-page-scroll`设置为true时生效,使用页面滚动而非内置scroll-view滚动,无需固定`z-paging`的高度,但需要在页面滚动到底部时调用`z-paging`的`doLoadMore()`方法。<br>当使用页面的下拉刷新时,需要引入mixin(可全局引入),具体可参见demo。 | 创建nvue页面并引入`z-paging`且运行在APP上生效,`z-paging`将使用nvue独有的`<list>`和`<refresh>`代替原有的scroll-view和自定义的下拉刷新,可大幅提升性能。 |
-|  **性能**  |                             不佳                             |                             一般                             |                              优                              |
-| **优缺点** | 【优点】配置简单、耦合度低。普通的简单列表不会有明显卡顿。<br/>【缺点】需要固定`z-paging`高度,超出页面部分渲染的资源无法自动回收,当列表item比较复杂或数据量过多时,可能会造成明显卡顿。 | 【优点】性能优于使用内置的scroll-view滚动,超出页面部分渲染的资源会自动回收,能适应绝大多数列表滚动的情况,即使列表item比较复杂,一般也不会感知到卡顿。<br>【缺点】配置略麻烦,耦合度较高。 | 【优点】原生渲染,极致性能,`<list>`组件在不可见部分的渲染资源回收有特殊的优化处理,`<refresh>`组件是app端独有的下拉刷新组件,性能远超普通vue页面中的自定义下拉刷新。<br>【缺点】仅App端支持,nvue页面写法不如vue页面方便,在`z-paging`中一些配置和方法在nvue中不支持,且nvue页面中支持的第三方组件也比vue页面少。 |
-
-#### 【总结】
-
-* 如果项目列表item比较简单,分页数据量不是特别多,建议使用默认的「内置scroll-view滚动」。
-* 如果项目列表item比较复杂,数据量多,且使用「内置scroll-view滚动」时卡顿明显,建议使用页面滚动。
-* 如果是App项目,且对性能和细节有较高要求,建议在nvue中使用`z-paging`。
-
-## 注意事项及常见问题
-
-* 【若无法下拉刷新】请确认要在@query所绑定的方法中调用`this.$refs.paging.complete()`,无论是否需要网络请求都要调用,只有告知z-paging刷新状态结束了,才可以开始下次的下拉刷新。
-
-* 【使用内置scroll-view滚动时】z-paging必须有确定的高度!否则上拉加载更多将无法触发,建议设置`:fixed=true`即可不设置高度!!(不希望跟着滚动的view可以设置`slot="top"`)。
-
-* 【使用页面滚动时】使用z-paging内置的scroll-view滚动性能不及使用页面的滚动。若您要使用页面的滚动,请勿固定z-paging的高度,并且必须设置`use-page-scroll`为true,否则将导致页面无法滚动(不希望跟着滚动的view可以设置`slot="top"`)。
-
-* 【使用页面滚动时】必须引入mixin(可全局引入)(具体可参照demo中的`page-default-demo.vue`文件)
-
-  或在页面的`onReachBottom`事件中调用`this.$refs.paging.doLoadMore()`且在`onPageScroll(e)`事件中调用`this.$refs.paging.updatePageScrollTop(e.scrollTop)`。(具体可参照demo中的`page-default-demo.vue`文件)
-
-* 【出现实际上有更多数据,而显示没有更多数据时】默认的pageSize(每页显示数量)为10,如果您服务端不需要传pageSize(例如有默认的pageSize:8),则您需要将默认的pageSize改成您与后端约定好的(8),若没有修改,则z-paging会认为传给服务端的pageSize是10,而服务端只返回了8条,因此会直接判定为没有更多数据。
-
-* 【若页面无法滚动】请检查z-paging是否有固定的高度;若您想使用页面滚动而非z-paging内置的scroll-view的滚动,请设置`use-page-scroll`为true。
-
-* 【关于自定义导航栏】若设置了`:fixed=true`,则必须将自定义导航栏放在z-paging标签中且添加slot="top",如:`<custom-nav slot="top"></custom-nav>`,如果有多个需要固定顶部的元素,则书写`<view slot="top">需要固定顶部的元素</view>`。
-
-### API文档地址:[http://z-paging.com](http://z-paging.com)
-### 备用API文档地址:[https://www.kancloud.cn/zxlee/z-paging/content](https://www.kancloud.cn/zxlee/z-paging/content) 
+| ![](https://z-paging.zxlee.cn/public/img/code.png) |

+ 2 - 0
uni_modules/z-tabs/changelog.md

@@ -0,0 +1,2 @@
+## 0.2.2(2022-10-11)
+修复在设置了`position: sticky`的view中使用`z-tabs`时,在App和微信小程序中可能出现的底部bar丢失的问题

+ 4 - 0
uni_modules/z-tabs/components/z-tabs/config/index.js

@@ -0,0 +1,4 @@
+// z-tabs全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
+export default {
+	
+}

+ 721 - 0
uni_modules/z-tabs/components/z-tabs/z-tabs.vue

@@ -0,0 +1,721 @@
+<!-- z-tabs v0.2.2 by-ZXLee -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-tabs -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?name=z-tabs -->
+<!-- 反馈QQ群:790460711 -->
+
+<template name="z-tabs">
+	<view class="z-tabs-conatiner" :style="[{background:bgColor}, tabsStyle]">
+		<view class="z-tabs-left">
+			<slot name="left" />
+		</view>
+		<view ref="z-tabs-scroll-view-conatiner" class="z-tabs-scroll-view-conatiner">
+			<scroll-view ref="z-tabs-scroll-view" class="z-tabs-scroll-view" :scroll-x="true" :scroll-left="scrollLeft" :show-scrollbar="false" :scroll-with-animation="isFirstLoaded" @scroll="scroll">
+				<view class="z-tabs-list-container" :style="[tabsListStyle]">
+					<view class="z-tabs-list" :style="[tabsListStyle, {marginTop: -finalBottomSpace+'px'}]">
+						<view :ref="`z-tabs-item-${index}`" :id="`z-tabs-item-${index}`" class="z-tabs-item" :style="[tabStyle]" v-for="(item,index) in list" :key="index" @click="tabsClick(index,item)">
+							<view class="z-tabs-item-title-container">
+								<text :class="{'z-tabs-item-title':true,'z-tabs-item-title-disabled':item.disabled}" 
+									:style="[{color:item.disabled?disabledColor:(currentIndex===index?activeColor:inactiveColor)},item.disabled?disabledStyle:(currentIndex===index?activeStyle:inactiveStyle)]">
+									{{item[nameKey]||item}}
+								</text>
+								<text v-if="item.badge&&_formatCount(item.badge.count).length" class="z-tabs-item-badge" :style="[badgeStyle]">{{_formatCount(item.badge.count)}}</text>
+							</view>
+						</view>
+					</view>
+					<view class="z-tabs-bottom" :style="[{width: tabsContainerWidth+'px', bottom: finalBottomSpace+'px'}]">
+						<view ref="z-tabs-bottom-dot" class="z-tabs-bottom-dot"
+						<!-- #ifndef APP-NVUE -->
+						:style="[{transform:`translateX(${bottomDotX}px)`,transition:dotTransition,background:activeColor},finalDotStyle]"
+						<!-- #endif -->
+						<!-- #ifdef APP-NVUE -->
+						:style="[{background:activeColor},finalDotStyle]"
+						<!-- #endif -->
+						/>
+					</view>	
+				</view>
+			</scroll-view>
+		</view>
+		<view class="z-tabs-right">
+			<slot name="right" />
+		</view>
+		
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const weexDom = weex.requireModule('dom');
+	const weexAnimation = weex.requireModule('animation');
+	// #endif
+	import zTabsConfig from './config/index'
+	
+	//获取默认配置信息
+	function _gc(key, defaultValue) {
+		let config = null;
+		if (zTabsConfig && Object.keys(zTabsConfig).length) {
+			config = zTabsConfig;
+		} else {
+			return defaultValue;
+		}
+		const value = config[_toKebab(key)];
+		return value === undefined ? defaultValue : value;
+	}
+	//驼峰转短横线
+	function _toKebab(value) {
+		return value.replace(/([A-Z])/g, "-$1").toLowerCase();
+	}
+	
+	/**
+	 * z-tabs 标签
+	 * @description 一个简单轻量的tabs标签,全平台兼容,支持nvue、vue3
+	 * @tutorial https://ext.dcloud.net.cn/plugin?name=z-tabs
+	 * @property {Array} list 数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
+	 * @property {Number|String} current 当前选中的index,默认为0
+	 * @property {Number|String} scroll-count list数组长度超过scrollCount时滚动显示(不自动铺满全屏),默认为5
+	 * @property {Number|String} tab-width 自定义每个tab的宽度,默认为0,即代表根据内容自动撑开,单位rpx,支持传100、"100px"或"100rpx"
+	 * @property {Number|String} bar-width 滑块宽度,单位rpx,支持传100、"100px"或"100rpx"
+	 * @property {Number|String} bar-height 滑块高度,单位rpx,支持传100、"100px"或"100rpx"
+	 * @property {Object} bar-style 滑块样式,其中的width和height将被bar-width和bar-height覆盖
+	 * @property {Number|String} bottom-space tabs与底部的间距,单位rpx,支持传100、"100px"或"100rpx"
+	 * @property {String} bar-animate-mode 切换tab时滑块动画模式,与swiper联动时有效,点击切换tab时无效,必须调用setDx。默认为line,即切换tab时滑块宽度保持不变,线性运动。可选值为worm,即为类似毛毛虫蠕动效果
+	 * @property {String} name-key list中item的name(标题)的key,默认为name
+	 * @property {String} value-key list中item的value的key,默认为value
+	 * @property {String} active-color 激活状态tab的颜色
+	 * @property {String} inactive-color 未激活状态tab的颜色
+	 * @property {String} disabled-color 禁用状态tab的颜色
+	 * @property {Object} active-style 激活状态tab的样式
+	 * @property {Object} inactive-style 未激活状态tab的样式
+	 * @property {Object} disabled-style 禁用状态tab的样式
+	 * @property {Number|String} badge-max-count 徽标数最大数字限制,超过这个数字将变成badge-max-count+,默认为99
+	 * @property {Object} badge-style 徽标样式,例如可自定义背景色,字体等等
+	 * @property {String} bg-color z-tabs背景色
+	 * @property {Object} tabs-style z-tabs样式
+	 * @property {Boolean} init-trigger-change 初始化时是否自动触发change事件
+	 * @event {Function(index,value)} change tabs改变时触发,index:当前切换到的index;value:当前切换到的value
+	 * @example <z-tabs :list="list"></z-tabs>
+	 */
+	export default {
+		name: 'z-tabs',
+		data() {
+			return {
+				currentIndex: 0,
+				currentSwiperIndex: 0,
+				bottomDotX: -1,
+				bottomDotXForIndex: 0,
+				showBottomDot: false,
+				shouldSetDx: true,
+				barCalcedWidth: 0,
+				pxBarWidth: 0,
+				scrollLeft: 0,
+				tabsSuperWidth: uni.upx2px(750),
+				tabsWidth: uni.upx2px(750),
+				tabsHeight: uni.upx2px(80),
+				tabsLeft: 0,
+				tabsContainerWidth: 0,
+				itemNodeInfos: [],
+				isFirstLoaded: false,
+				currentScrollLeft: 0,
+				changeTriggerFailed: false
+			};
+		},
+		props: {
+			//数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
+			list: {
+				type: Array,
+				default: function() {
+					return [];
+				}
+			},
+			//当前选中的index
+			current: {
+				type: [Number, String],
+				default: _gc('current',0)
+			},
+			//list数组长度超过scrollCount时滚动显示(不自动铺满全屏)
+			scrollCount: {
+				type: [Number, String],
+				default: _gc('scrollCount',5)
+			},
+			//z-tabs样式
+			tabsStyle: {
+				type: Object,
+				default: function() {
+					return _gc('tabsStyle',{})
+				}
+			},
+			//自定义每个tab的宽度,默认为0,即代表根据内容自动撑开,单位rpx,支持传100、"100px"或"100rpx"
+			tabWidth: {
+				type: [Number, String],
+				default: _gc('tabWidth',0)
+			},
+			//滑块宽度,单位rpx,支持传100、"100px"或"100rpx"
+			barWidth: {
+				type: [Number, String],
+				default: _gc('barWidth',45)
+			},
+			//滑块高度,单位rpx,支持传100、"100px"或"100rpx"
+			barHeight: {
+				type: [Number, String],
+				default: _gc('barHeight',8)
+			},
+			//滑块样式,其中的width和height将被barWidth和barHeight覆盖
+			barStyle: {
+				type: Object,
+				default: function() {
+					return _gc('barStyle',{});
+				}
+			},
+			//tabs与底部的间距,单位rpx,支持传100、"100px"或"100rpx"
+			bottomSpace: {
+				type: [Number, String],
+				default: _gc('bottomSpace',8)
+			},
+			//切换tab时滑块动画模式,与swiper联动时有效,点击切换tab时无效,必须调用setDx。默认为line,即切换tab时滑块宽度保持不变,线性运动。可选值为worm,即为类似毛毛虫蠕动效果
+			barAnimateMode: {
+				type: String,
+				default: _gc('barAnimateMode','line')
+			},
+			//list中item的name(标题)的key
+			nameKey: {
+				type: String,
+				default: _gc('nameKey','name')
+			},
+			//list中item的value的key
+			valueKey: {
+				type: String,
+				default: _gc('valueKey','value')
+			},
+			//激活状态tab的颜色
+			activeColor: {
+				type: String,
+				default: _gc('activeColor','#007AFF')
+			},
+			//未激活状态tab的颜色
+			inactiveColor: {
+				type: String,
+				default: _gc('inactiveColor','#666666')
+			},
+			//禁用状态tab的颜色
+			disabledColor: {
+				type: String,
+				default: _gc('disabledColor','#bbbbbb')
+			},
+			//激活状态tab的样式
+			activeStyle: {
+				type: Object,
+				default: function() {
+					return _gc('activeStyle',{});
+				}
+			},
+			//未激活状态tab的样式
+			inactiveStyle: {
+				type: Object,
+				default: function() {
+					return _gc('inactiveStyle',{});
+				}
+			},
+			//禁用状态tab的样式
+			disabledStyle: {
+				type: Object,
+				default: function() {
+					return _gc('disabledStyle',{});
+				}
+			},
+			//z-tabs背景色
+			bgColor: {
+				type: String,
+				default: _gc('bgColor','white')
+			},
+			//徽标数最大数字限制,超过这个数字将变成badgeMaxCount+
+			badgeMaxCount: {
+				type: [Number, String],
+				default: _gc('badgeMaxCount',99)
+			},
+			//徽标样式,例如可自定义背景色,字体等等
+			badgeStyle: {
+				type: Object,
+				default: function() {
+					return _gc('badgeStyle',{})
+				}
+			},
+			//初始化时是否自动触发change事件
+			initTriggerChange: {
+				type: Boolean,
+				default: _gc('initTriggerChange',false)
+			},
+		},
+		mounted() {
+			this.updateSubviewLayout();
+		},
+		watch: {
+			current: {
+				handler(newVal) {
+					this.currentIndex = newVal;
+					this._preUpdateDotPosition(this.currentIndex);
+					if (this.initTriggerChange) {
+						if (newVal < this.list.length) {
+							this.$emit('change', newVal, this.list[newVal][this.valueKey]);
+						}else {
+							this.changeTriggerFailed = true;
+						}
+					}
+				},
+				immediate: true
+			},
+			list: {
+				handler(newVal) {
+					this._handleListChange(newVal);
+				},
+				immediate: false
+			},
+			bottomDotX(newVal) {
+				if(newVal >= 0){
+					// #ifndef APP-NVUE
+					this.showBottomDot = true;
+					// #endif
+					this.$nextTick(() => {
+						// #ifdef APP-NVUE
+						weexAnimation.transition(this.$refs['z-tabs-bottom-dot'], {
+							styles: {
+								transform: `translateX(${newVal}px)`
+							},
+							duration: this.showAnimate ? 200 : 0,
+							delay: 0
+						})
+						setTimeout(() => {
+							this.showBottomDot = true;
+						},10)
+						// #endif
+					})
+				}
+			},
+			finalBarWidth: {
+				handler(newVal) {
+					this.barCalcedWidth = newVal;
+					this.pxBarWidth = this.barCalcedWidth;
+				},
+				immediate: true
+			},
+			currentIndex: {
+				handler(newVal) {
+					this.currentSwiperIndex = newVal;
+				},
+				immediate: true
+			}
+		},
+		computed: {
+			shouldScroll(){
+				return this.list.length > this.scrollCount;
+			},
+			finalTabsHeight(){
+				return this.tabsHeight;
+			},
+			tabStyle(){
+				const stl = this.shouldScroll ? {'flex-shrink': 0} : {'flex': 1};
+				if(this.finalTabWidth > 0){
+					stl['width'] = this.finalTabWidth + 'px';
+				}else{
+					delete stl.width;
+				} 
+				return stl;
+			},
+			tabsListStyle(){
+				return this.shouldScroll ? {} : {'flex':1};
+			},
+			showAnimate(){
+				return this.isFirstLoaded && !this.shouldSetDx;
+			},
+			dotTransition(){
+				return this.showAnimate ? 'transform .2s linear':'none';
+			},
+			finalDotStyle(){
+				return {...this.barStyle, width: this.barCalcedWidth + 'px', height: this.finalBarHeight + 'px', opacity: this.showBottomDot ? 1 : 0};
+			},
+			finalTabWidth(){
+				return this._convertTextToPx(this.tabWidth);
+			},
+			finalBarWidth(){
+				return this._convertTextToPx(this.barWidth);
+			},
+			finalBarHeight(){
+				return this._convertTextToPx(this.barHeight);
+			},
+			finalBottomSpace(){
+				return this._convertTextToPx(this.bottomSpace);
+			}
+		},
+		methods: {
+			//根据swiper的@transition实时更新底部dot位置
+			setDx(dx) {
+				if (!this.shouldSetDx) return;
+				const isLineMode = this.barAnimateMode === 'line';
+				const isWormMode = this.barAnimateMode === 'worm';
+				let dxRate = dx / this.tabsSuperWidth;
+				this.currentSwiperIndex = this.currentIndex + parseInt(dxRate);
+				const isRight = dxRate > 0;
+				const barWidth = this.pxBarWidth;
+				if(this.currentSwiperIndex !== this.currentIndex){
+					dxRate = dxRate - (this.currentSwiperIndex - this.currentIndex);
+					this.bottomDotXForIndex = this._getBottomDotX(this.itemNodeInfos[this.currentSwiperIndex], barWidth);
+				}
+				const currentIndex = this.currentSwiperIndex;
+				let nextIndex = currentIndex + (isRight ? 1 : -1);
+				nextIndex = Math.max(0, nextIndex);
+				nextIndex = Math.min(nextIndex, this.itemNodeInfos.length - 1);
+				const currentNodeInfo = this.itemNodeInfos[currentIndex];
+				const nextNodeInfo = this.itemNodeInfos[nextIndex];
+				const nextBottomX = this._getBottomDotX(nextNodeInfo, barWidth);
+				if (isLineMode){
+					this.bottomDotX = this.bottomDotXForIndex + (nextBottomX - this.bottomDotXForIndex) * Math.abs(dxRate);
+				} else if (isWormMode) {
+					if ((isRight && currentIndex >= this.itemNodeInfos.length - 1) || (!isRight && currentIndex <= 0)) return;
+					const spaceOffset = isRight ? nextNodeInfo.right - currentNodeInfo.left : currentNodeInfo.right - nextNodeInfo.left;
+					let barCalcedWidth = barWidth + spaceOffset * Math.abs(dxRate);
+					if (isRight) {
+						if (barCalcedWidth > nextBottomX - this.bottomDotX + barWidth) {
+							const barMinusWidth = barWidth + spaceOffset * (1 - dxRate);
+							this.bottomDotX = this.bottomDotXForIndex + (barCalcedWidth - barMinusWidth) / 2;
+							barCalcedWidth = barMinusWidth;
+						}
+					}else if (!isRight) {
+						if (barCalcedWidth > this.bottomDotXForIndex + barWidth - nextBottomX){
+							const barMinusWidth = barWidth + spaceOffset * (1 + dxRate);
+							barCalcedWidth = barMinusWidth;
+							this.bottomDotX = nextBottomX;
+						} else{
+							this.bottomDotX = this.bottomDotXForIndex - (barCalcedWidth - barWidth);
+						}
+					}
+					barCalcedWidth = Math.max(barCalcedWidth, barWidth);
+					this.barCalcedWidth = barCalcedWidth;
+				}
+			},
+			//在swiper的@animationfinish中通知z-tabs结束多setDx的锁定,若在父组件中调用了setDx,则必须调用unlockDx
+			unlockDx() {
+				this.shouldSetDx = true;
+			},
+			//更新z-tabs内部布局
+			updateSubviewLayout(tryCount = 0) {
+				this.$nextTick(() => {
+					let delayTime = 10;
+					// #ifdef APP-NVUE || MP-BAIDU
+					delayTime = 50;
+					// #endif
+					setTimeout(() => {
+						this._getNodeClientRect('.z-tabs-scroll-view-conatiner').then(res=>{
+							if (res){ 
+								if (!res[0].width && tryCount < 10) {
+									setTimeout(() => {
+										tryCount ++;
+										this.updateSubviewLayout(tryCount);
+									}, 50);
+									return;
+								}
+								this.tabsWidth = res[0].width;
+								this.tabsHeight = res[0].height;
+								this.tabsLeft = res[0].left;
+								this._handleListChange(this.list);
+							}
+						})
+						this._getNodeClientRect('.z-tabs-conatiner').then(res=>{
+							if(res && res[0].width){
+								this.tabsSuperWidth = res[0].width;
+							}
+						})
+					},delayTime)
+				})
+			},
+			//点击了tabs
+			tabsClick(index,item) {
+				if (item.disabled) return;
+				if (this.currentIndex != index) {
+					this.shouldSetDx = false;
+					this.$emit('change', index, item[this.valueKey]);
+					this.currentIndex = index;
+					this._preUpdateDotPosition(index);
+				}
+			},
+			//scroll-view滚动
+			scroll(e){
+				this.currentScrollLeft = e.detail.scrollLeft;
+			},
+			//更新底部dot位置之前的预处理
+			_preUpdateDotPosition(index) {
+				// #ifndef APP-NVUE
+				this.$nextTick(() => {
+					uni.createSelectorQuery().in(this).select(".z-tabs-scroll-view").fields({
+					  scrollOffset: true
+					}, data => {
+						if (data) {
+							this.currentScrollLeft = data.scrollLeft;
+							this._updateDotPosition(index);
+						} else {
+							this._updateDotPosition(index);
+						}
+					}).exec();
+				})
+				// #endif
+				
+				// #ifdef APP-NVUE
+				this._updateDotPosition(index);
+				// #endif
+			},
+			//更新底部dot位置
+			_updateDotPosition(index){
+				if(index >= this.itemNodeInfos.length) return;
+				this.$nextTick(async ()=>{
+					let node = this.itemNodeInfos[index];
+					let offset = 0;
+					let tabsContainerWidth = this.tabsContainerWidth;
+					if (JSON.stringify(this.activeStyle) !== '{}') {
+						const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${index}`,true);
+						if (nodeRes) {
+							node = nodeRes[0];
+							offset = this.currentScrollLeft;
+							this.tabsHeight = Math.max(node.height + uni.upx2px(28), this.tabsHeight);
+							tabsContainerWidth = 0;
+							for(let i = 0;i < this.itemNodeInfos.length;i++){
+								let oldNode = this.itemNodeInfos[i];
+								tabsContainerWidth += i === index ? node.width : oldNode.width;
+							}
+						}
+					}
+					this.bottomDotX = this._getBottomDotX(node, this.finalBarWidth, offset);
+					this.bottomDotXForIndex = this.bottomDotX;
+					if (this.tabsWidth) {
+						setTimeout(()=>{
+							let scrollLeft = this.bottomDotX - this.tabsWidth / 2 + this.finalBarWidth / 2;
+							scrollLeft = Math.max(0,scrollLeft);
+							if (tabsContainerWidth) {
+								scrollLeft = Math.min(scrollLeft,tabsContainerWidth - this.tabsWidth + 10);
+							}
+							if (this.shouldScroll && tabsContainerWidth > this.tabsWidth) {
+								this.scrollLeft = scrollLeft;
+							}
+							this.$nextTick(()=>{
+								this.isFirstLoaded = true;
+							})
+						},200)
+					}
+				})
+			},
+			// 处理list改变
+			_handleListChange(newVal) {
+				this.$nextTick(async ()=>{
+					if(newVal.length){
+						let itemNodeInfos = [];
+						let tabsContainerWidth = 0;
+						let delayTime = 0;
+						// #ifdef MP-BAIDU
+						delayTime = 100;
+						// #endif
+						setTimeout(async()=>{
+							for(let i = 0;i < newVal.length;i++){
+								const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${i}`,true);
+								if(nodeRes){
+									const node = nodeRes[0];
+									node.left += this.currentScrollLeft;
+									itemNodeInfos.push(node);
+									tabsContainerWidth += node.width;
+								}
+								if (i === this.currentIndex){
+									this.itemNodeInfos = itemNodeInfos;
+									this.tabsContainerWidth = tabsContainerWidth;
+									this._updateDotPosition(this.currentIndex);
+								}
+							}
+							this.itemNodeInfos = itemNodeInfos;
+							this.tabsContainerWidth = tabsContainerWidth;
+							this._updateDotPosition(this.currentIndex);
+						},delayTime)
+					}
+				})
+				
+				if (this.initTriggerChange && this.changeTriggerFailed && newVal.length) {
+					if (this.current < newVal.length) {
+						this.$emit('change', this.current, newVal[this.current][this.valueKey]);
+					}
+				}
+			},
+			//根据node获取bottomX
+			_getBottomDotX(node, barWidth = this.finalBarWidth, offset = 0){
+				return node.left + node.width / 2 - barWidth / 2 + offset - this.tabsLeft;
+			},
+			//获取节点信息
+			_getNodeClientRect(select, withRefArr = false) {
+				// #ifdef APP-NVUE
+				select = select.replace('.', '').replace('#', '');
+				const ref = withRefArr ? this.$refs[select][0] : this.$refs[select];
+				return new Promise((resolve, reject) => {
+					if (ref) {
+						weexDom.getComponentRect(ref, option => {
+							if (option && option.result) {
+								resolve([option.size]);
+							} else resolve(false);
+						})
+					} else resolve(false);
+				});
+				return;
+				// #endif
+				const res = uni.createSelectorQuery().in(this);
+				res.select(select).boundingClientRect();
+				return new Promise((resolve, reject) => {
+					res.exec(data => {
+						resolve((data && data != '' && data != undefined && data.length) ? data : false);
+					});
+				});
+			},
+			//格式化badge中的count
+			_formatCount(count) {
+				if (!count) return '';
+				if (count > this.badgeMaxCount) {
+					return this.badgeMaxCount + '+';
+				}
+				return count.toString();
+			},
+			//将文本的px或者rpx转为px的值
+			_convertTextToPx(text) {
+				const dataType = Object.prototype.toString.call(text);
+				if (dataType === '[object Number]') {
+					return uni.upx2px(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', '');
+				} else {
+					text = uni.upx2px(text);
+				}
+				if (!isNaN(text)) {
+					if (isRpx) return Number(uni.upx2px(text));
+					return Number(text);
+				}
+				return 0;
+			}
+		}
+		
+	}
+</script>
+
+<style scoped>
+	.z-tabs-conatiner{
+		/* #ifndef APP-NVUE */
+		overflow: hidden;
+		display: flex;
+		width: 100%;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 750rpx;
+		/* #endif */
+		flex-direction: row;
+		height: 103rpx;
+	}
+	
+	.z-tabs-scroll-view-conatiner{
+		flex: 1;
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		height: 100%;
+		width: 100%;
+		/* #endif */
+		flex-direction: row;
+	}
+	
+	/* #ifndef APP-NVUE */
+	.z-tabs-scroll-view ::-webkit-scrollbar {
+		display: none;
+		-webkit-appearance: none;
+		width: 0 !important;
+		height: 0 !important;
+		background: transparent;
+	}
+	/* #endif */
+	
+	.z-tabs-scroll-view{
+		flex-direction: row;
+		position: absolute;
+		left: 0;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		height: 100%;
+		/* #endif */
+		flex: 1;
+	}
+	
+	.z-tabs-list-container{
+		position: relative;
+		/* #ifndef APP-NVUE */
+		height: 100%;
+		/* #endif */
+	}
+	
+	.z-tabs-list,.z-tabs-list-container{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+	
+	.z-tabs-item{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		padding: 0px 20rpx;
+	}
+	
+	.z-tabs-item-title-container{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+	}
+	
+	.z-tabs-item-title{
+		font-size: 30rpx;
+	}
+	
+	.z-tabs-item-title-disabled{
+		/* #ifndef APP-NVUE */
+		cursor: not-allowed;
+		/* #endif */
+	}
+	
+	.z-tabs-item-badge{
+		margin-left: 8rpx;
+		background-color: #ec5b56;
+		color: white;
+		font-size: 22rpx;
+		border-radius: 100px;
+		padding: 0rpx 10rpx;
+	}
+	
+	.z-tabs-bottom{
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		right: 0;
+	}
+	
+	.z-tabs-bottom-dot{
+		border-radius: 100px;
+	}
+	
+	.z-tabs-left,.z-tabs-right{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+	}
+</style>
+

+ 82 - 0
uni_modules/z-tabs/package.json

@@ -0,0 +1,82 @@
+{
+  "id": "z-tabs",
+  "name": "z-tabs",
+  "displayName": "【z-tabs】一个简单轻量的tabs组件",
+  "version": "0.2.2",
+  "description": "全平台兼容,支持nvue、vue3",
+  "keywords": [
+    "tabs"
+],
+  "repository": "https://github.com/SmileZXLee/uni-z-tabs",
+  "engines": {
+    "HBuilderX": "^3.0.7"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": "393727164"
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@zxlee/z-tabs",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y",
+		  "钉钉": "y",
+		  "快手": "y",
+		  "飞书": "y",
+		  "京东": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 174 - 0
uni_modules/z-tabs/readme.md

@@ -0,0 +1,174 @@
+# z-tabs
+
+[![version](https://img.shields.io/badge/version-0.2.2-blue)](https://github.com/SmileZXLee/uni-z-tabs)
+[![license](https://img.shields.io/github/license/SmileZXLee/uni-z-tabs)](https://en.wikipedia.org/wiki/MIT_License)
+
+***
+
+### 反馈qq群(点击加群):[790460711](https://jq.qq.com/?_wv=1027&k=vU2fKZZH)
+
+***
+
+## z-tabs文档
+
+### 安装
+
+#### 方式1(推荐):通过uni_modules安装,在插件市场中点击右上角【使用HbuilderX导入插件】即可。 
+
+***
+
+#### 方式2:通过npm安装  
+
+```bash
+//若项目之前未使用npm管理依赖(项目根目录下无package.json文件),先在项目根目录执行命令初始化npm工程
+npm init -y
+
+//安装
+npm install @zxlee/z-tabs --save
+//更新
+npm update @zxlee/z-tabs
+```
+
+然后在`pages.json`中配置`easycom`(注意:下方配置只有在使用npm安装时才需要配置!!!!!)  
+
+```json
+"easycom": {
+    "^z-tabs": "@zxlee/z-tabs/components/z-tabs/z-tabs.vue"
+}
+```
+
+### 基本使用
+
+```html
+<template>
+	<z-tabs :list="list"></z-tabs>
+</template>
+
+<script>
+    export default {
+        data() {
+            return {
+                list: []
+            }
+        },
+        onLoad() {
+            const list = [];
+            for(let i = 0;i < 10;i++) {
+				// list内item支持字符串或对象,下方这个是字符串
+				list.push('tab标题');
+
+				// 如果要展示徽标数,则list中item的数据结构应为:
+				list.push({
+					name: 'tab标题',
+					badge: {
+						// 设置徽标数为6
+						count: 6
+					},
+					// 可以禁用某个item
+					disabled: true
+				});
+            }
+			this.list = list;
+		}
+    }
+</script>
+```
+
+
+
+### props
+
+| 参数                | 说明                                                         | 类型           | 默认值  | 可选值 |
+| :------------------ | :----------------------------------------------------------- | :------------- | :------ | :----- |
+| list                | 数据源数组,支持形如`['tab1','tab2']`的格式或`[{name:'tab1',value:1}]`的格式 | Array          | []      | -      |
+| current             | 当前选中的index                                              | Number\|String | 0       | -      |
+| scroll-count        | list数组长度超过scrollCount时滚动显示(不自动铺满全屏)        | Number\|String | 5       | -      |
+| tab-width           | 自定义每个tab的宽度,默认为0,即代表根据内容自动撑开,单位rpx,支持传100、"100px"或"100rpx" | Number\|String | 0       | 0      |
+| bar-width           | 滑块宽度,单位rpx,支持传100、"100px"或"100rpx"              | Number\|String | 45rpx   | -      |
+| bar-height          | 滑块高度,单位rpx,支持传100、"100px"或"100rpx"              | Number\|String | 8rpx    | -      |
+| bar-style           | 滑块样式,其中的`width`和`height`将被`bar-width`和`bar-height`覆盖 | Object         | {}      | -      |
+| bottom-space        | tabs与底部的间距,单位rpx,支持传100、"100px"或"100rpx"      | Number\|String | 8rpx    | -      |
+| bar-animate-mode    | 【v0.1.5起支持】切换tab时滑块动画模式,与`swiper`联动时有效,点击切换tab时无效,必须调用`setDx`。默认为`line`,即切换tab时滑块宽度保持不变,线性运动。可选值为`worm`,即为类似毛毛虫蠕动效果 | String         | line    | worm   |
+| name-key            | list中item的name(标题)的key                                  | String         | name    | -      |
+| value-key           | list中item的value的key                                       | String         | value   | -      |
+| active-color        | 激活状态tab的颜色                                            | String         | #007AFF | -      |
+| inactive-color      | 未激活状态tab的颜色                                          | String         | #666666 | -      |
+| disabled-color      | 禁用状态tab的颜色                                            | String         | #bbbbbb | -      |
+| active-style        | 激活状态tab的样式                                            | Object         | {}      | -      |
+| inactive-style      | 未激活状态tab的样式                                          | Object         | {}      | -      |
+| disabled-style      | 禁用状态tab的样式                                            | Object         | {}      | -      |
+| badge-max-count     | 徽标数最大数字限制,超过这个数字将变成`badge-max-count`+     | Number\|String | 99      | -      |
+| badge-style         | 徽标样式,例如可自定义背景色,字体等等                       | Object         | {}      | -      |
+| bg-color            | z-tabs背景色                                                 | String         | white   | -      |
+| tabs-style          | z-tabs样式                                                   | Object         | {}      | -      |
+| init-trigger-change | 初始化时是否自动触发change事件                               | Boolean        | true    | false  |
+
+
+### events
+
+| 事件名  | 说明                 | 回调参数                                                     |
+| ------- | -------------------- | ------------------------------------------------------------ |
+| @change | tabs改变(点击)时触发 | `参数1`:index(当前切换到的index);<br/>`参数2`:value(当前切换到的value) |
+
+### methods
+
+| 方法名              | 说明                                                         | 参数                                   |
+| ------------------- | ------------------------------------------------------------ | -------------------------------------- |
+| setDx               | 根据swiper的`@transition`实时更新底部dot位置                 | swiper的`@transition`中的`e.detail.dx` |
+| unlockDx            | 在swiper的`@animationfinish`中通知`z-tabs`结束多`setDx`的锁定,若在父组件中调用了`setDx`,则必须调用`unlockDx` | -                                      |
+| updateSubviewLayout | 在nvue+安卓中,若在cell中使用`z-tabs`,且页面加载时cell在屏幕之外,因cell的复用机制,可能导致`z-tabs`内部的布局失效:例如底部bar无法显示,此时可在list滚动到一定区域内(例如快显示`z-tabs`)的时候调用此方法以更新其内部布局。其他情况无需调用! | -                                      |
+
+### slots
+
+| 名称  | 说明         |
+| :---- | ------------ |
+| left  | tabs左侧插槽 |
+| right | tabs右侧插槽 |
+
+### 支持全局配置
+
+* 在`/z-tabs/components/z-tabs/config/index.js`文件中进行配置
+
+```js
+export default {
+	'active-color': 'red'
+}
+```
+
+### 【v0.1.4起支持】底部dot与swiper联动演示
+
+```html
+<template>
+  <z-tabs ref="tabs" :list="tabList" :current="current" @change="tabsChange" />
+  <swiper :current="current" @transition="swiperTransition" @animationfinish="swiperAnimationfinish">
+    <swiper-item class="swiper-item" v-for="(item, index) in tabList" :key="index">
+      xxx
+    </swiper-item>
+  </swiper>
+<template>
+<script>
+	export default {
+		data() {
+			return {
+				tabList: ['测试1','测试2','测试3','测试4'],
+				current: 0, // tabs组件的current值,表示当前活动的tab选项
+			};
+		},
+		methods: {
+			//tabs通知swiper切换
+			tabsChange(index) {
+				this.current = index;
+			},
+			//swiper滑动中
+			swiperTransition(e) {
+				this.$refs.tabs.setDx(e.detail.dx);
+			},
+			//swiper滑动结束
+			swiperAnimationfinish(e) {
+				this.current = e.detail.current;
+				this.$refs.tabs.unlockDx();
+			}
+		}
+	}
+</script>
+```

+ 1 - 1
uview-ui/components/u-read-more/u-read-more.vue

@@ -159,7 +159,7 @@
 		&__showmore-wrap {
 			position: relative;
 			width: 100%;
-			padding-bottom: 26rpx;
+			padding-bottom: 10rpx;
 			@include vue-flex;
 			align-items: center;
 			justify-content: center;

Some files were not shown because too many files changed in this diff