ext.liuqiwen3 2 months ago
parent
commit
b02a2a2862
85 changed files with 17510 additions and 2119 deletions
  1. 2 1
      components/addressWindow/index.vue
  2. 2 1
      components/couponListWindow/index.vue
  3. 1 1
      components/goodList/index.vue
  4. 1 1
      components/orderGoods/index.vue
  5. 1 1
      components/recommend/index.vue
  6. 2 2
      config/app.js
  7. 61 71
      pages.json
  8. 0 312
      pages/find_funds/findFunds.vue
  9. 0 1262
      pages/find_funds/fundsOrder.vue
  10. 0 0
      pages/find_funds/index.vue
  11. 5 5
      pages/goods_details/index.vue
  12. 0 215
      pages/goods_search/index.vue
  13. 2 2
      pages/index/index.vue
  14. 1 1
      pages/merchantCenter/index.vue
  15. 0 0
      pages/merchantCenters/merchant.vue
  16. 1 1
      pages/order_addcart/order_addcart.vue
  17. 3 3
      pages/user/index.vue
  18. 0 0
      pages/users/SVIP/SVIP.vue
  19. 2 2
      pages/VIP/VIP.vue
  20. 1 1
      pages/users/browsing_history/index.vue
  21. 248 235
      pages/users/order_confirm/index.vue
  22. 1 1
      pages/personal_info/personal_info.vue
  23. 1 1
      pages/users/user_goods_collection/index.vue
  24. 103 0
      pages/users/vault/aggrement.vue
  25. 1236 0
      pages/users/vault/index.vue
  26. 603 0
      pages/users/vault/recycle/order_fill.vue
  27. 416 0
      pages/users/vault/recycle/recyle_order.vue
  28. 398 0
      pages/users/vault/recycle/report.vue
  29. 701 0
      pages/users/vault/storeMetal/GoldMailForm.vue
  30. 363 0
      pages/users/vault/storeMetal/gmReport.vue
  31. 161 0
      pages/users/vault/storeMetal/goldBullionStock.vue
  32. 1103 0
      pages/users/vault/storeMetal/index.vue
  33. 817 0
      pages/users/vault/storeMetal/metalExchange.vue
  34. 508 0
      pages/users/vault/storeMetal/nonLogisticsGold.vue
  35. 355 0
      pages/users/vault/storeMetal/order.vue
  36. 0 0
      pages/users/vault/withdraw.vue
  37. BIN
      static/avator.png
  38. 51 0
      uni_modules/z-paging/changelog.md
  39. 47 0
      uni_modules/z-paging/components/z-paging-cell/z-paging-cell.vue
  40. 209 0
      uni_modules/z-paging/components/z-paging-empty-view/z-paging-empty-view.vue
  41. 160 0
      uni_modules/z-paging/components/z-paging-swiper-item/z-paging-swiper-item.vue
  42. 176 0
      uni_modules/z-paging/components/z-paging-swiper/z-paging-swiper.vue
  43. 182 0
      uni_modules/z-paging/components/z-paging/components/z-paging-load-more.vue
  44. 214 0
      uni_modules/z-paging/components/z-paging/components/z-paging-refresh.vue
  45. 3 0
      uni_modules/z-paging/components/z-paging/config/index.js
  46. 241 0
      uni_modules/z-paging/components/z-paging/css/z-paging-main.css
  47. 50 0
      uni_modules/z-paging/components/z-paging/css/z-paging-static.css
  48. 23 0
      uni_modules/z-paging/components/z-paging/i18n/en.json
  49. 8 0
      uni_modules/z-paging/components/z-paging/i18n/index.js
  50. 23 0
      uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json
  51. 23 0
      uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json
  52. 25 0
      uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js
  53. 25 0
      uni_modules/z-paging/components/z-paging/js/hooks/useZPagingComp.js
  54. 125 0
      uni_modules/z-paging/components/z-paging/js/modules/back-to-top.js
  55. 153 0
      uni_modules/z-paging/components/z-paging/js/modules/chat-record-mode.js
  56. 152 0
      uni_modules/z-paging/components/z-paging/js/modules/common-layout.js
  57. 744 0
      uni_modules/z-paging/components/z-paging/js/modules/data-handle.js
  58. 144 0
      uni_modules/z-paging/components/z-paging/js/modules/empty.js
  59. 113 0
      uni_modules/z-paging/components/z-paging/js/modules/i18n.js
  60. 374 0
      uni_modules/z-paging/components/z-paging/js/modules/load-more.js
  61. 95 0
      uni_modules/z-paging/components/z-paging/js/modules/loading.js
  62. 299 0
      uni_modules/z-paging/components/z-paging/js/modules/nvue.js
  63. 835 0
      uni_modules/z-paging/components/z-paging/js/modules/refresher.js
  64. 589 0
      uni_modules/z-paging/components/z-paging/js/modules/scroller.js
  65. 539 0
      uni_modules/z-paging/components/z-paging/js/modules/virtual-list.js
  66. 19 0
      uni_modules/z-paging/components/z-paging/js/z-paging-constant.js
  67. 45 0
      uni_modules/z-paging/components/z-paging/js/z-paging-enum.js
  68. 97 0
      uni_modules/z-paging/components/z-paging/js/z-paging-interceptor.js
  69. 537 0
      uni_modules/z-paging/components/z-paging/js/z-paging-main.js
  70. 22 0
      uni_modules/z-paging/components/z-paging/js/z-paging-mixin.js
  71. 13 0
      uni_modules/z-paging/components/z-paging/js/z-paging-static.js
  72. 322 0
      uni_modules/z-paging/components/z-paging/js/z-paging-utils.js
  73. 67 0
      uni_modules/z-paging/components/z-paging/wxs/z-paging-renderjs.js
  74. 382 0
      uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs
  75. 656 0
      uni_modules/z-paging/components/z-paging/z-paging.vue
  76. 89 0
      uni_modules/z-paging/package.json
  77. 57 0
      uni_modules/z-paging/readme.md
  78. 11 0
      uni_modules/z-paging/types/comps.d.ts
  79. 9 0
      uni_modules/z-paging/types/comps/_common.d.ts
  80. 29 0
      uni_modules/z-paging/types/comps/z-paging-cell.d.ts
  81. 95 0
      uni_modules/z-paging/types/comps/z-paging-empty-view.d.ts
  82. 95 0
      uni_modules/z-paging/types/comps/z-paging-swiper-item.d.ts
  83. 89 0
      uni_modules/z-paging/types/comps/z-paging-swiper.d.ts
  84. 2131 0
      uni_modules/z-paging/types/comps/z-paging.d.ts
  85. 24 0
      uni_modules/z-paging/types/index.d.ts

+ 2 - 1
components/addressWindow/index.vue

@@ -16,7 +16,7 @@
 			</view>
 			<!-- 无地址 -->
 			<view class='pictrue' v-if="!is_loading && !addressList.length">
-				<image src='/static/images/noAddress.png'></image>
+				<image :src="HTTP_REQUEST_URL_IMG+'noAddress.png'"></image>
 			</view>
 			<view class='addressBnt bg-color' @tap='goAddressPages'>添加地址</view>
 		</view>
@@ -27,6 +27,7 @@
 <script setup>
 import { ref } from 'vue';
 import { getAddressList } from '@/api/user.js';
+import { HTTP_REQUEST_URL_IMG } from "@/config/app";
 
 const props = defineProps({
   pagesUrl: {

+ 2 - 1
components/couponListWindow/index.vue

@@ -99,7 +99,7 @@
           </scroll-view>
           <!-- 无优惠券 -->
           <view class="pictrue" v-else>
-            <image src="/static/images/noCoupon.png"></image>
+            <image :src="HTTP_REQUEST_URL_IMG+'noCoupon.png'"></image>
           </view>
         </view>
       </view>
@@ -111,6 +111,7 @@
 import { ref } from "vue";
 import { setCouponReceive } from "@/api/api.js";
 import { useToast } from "@/hooks/useToast";
+import { HTTP_REQUEST_URL_IMG } from "@/config/app";
 
 const props = defineProps({
   showPopup: {

+ 1 - 1
components/goodList/index.vue

@@ -48,7 +48,7 @@ const goDetail = async (item) => {
   try {
     await goShopDetail(item, uid.value);
     uni.navigateTo({
-      url: `/pages/goods_details/index?id=${item.id}`,
+      url: `/pages/goods/goods_details/index?id=${item.id}`,
     });
   } catch (err) {
     console.error('Navigation error:', err);

+ 1 - 1
components/orderGoods/index.vue

@@ -97,7 +97,7 @@ function jumpCon(id) {
   let type = props.productType == 0 ? "normal" : "video";
   if (props.jump) {
     uni.navigateTo({
-      url: `/pages/goods_details/index?id=${id}&type=${type}`,
+      url: `/pages/goods/goods_details/index?id=${id}&type=${type}`,
     });
   }
 }

+ 1 - 1
components/recommend/index.vue

@@ -46,7 +46,7 @@ const goDetail = async (item) => {
   try {
     await goShopDetail(item, uid.value);
     uni.navigateTo({
-      url: `/pages/goods_details/index?id=${item.id}`,
+      url: `/pages/goods/goods_details/index?id=${item.id}`,
     });
   } catch (err) {
     console.error("Navigation error:", err);

+ 2 - 2
config/app.js

@@ -1,6 +1,6 @@
-let domain = "https://www.shuibeibyg.com/front-api"; // 正式环境IP
+// let domain = "https://www.shuibeibyg.com/front-api"; // 正式环境IP
 // let domain = "https://test.shuibeibyg.com/front-api"; // 测试环境IP
-// let domain = 'http://192.168.100.199:8081' // 晋守桦IP
+let domain = 'http://192.168.100.199:8081' // 晋守桦IP
 // let domain = 'http://192.168.100.246:8081' // 韩朝龙IP
 let share = "https://www.shuibeibyg.com";
 

+ 61 - 71
pages.json

@@ -41,47 +41,7 @@
 		    "enablePullDownRefresh": false
 		  }
 		},
-		{
-		  "path": "pages/find_funds/findFunds",
-		  "style": {
-		    "navigationBarTitleText": "找款",
-		    "navigationBarBackgroundColor": "#ffe079",
-		    "navigationBarTextStyle": "black",
-		    "app-plus": {
-		      "titleNView": {
-		        "type": "default"
-		      }
-		    }
-		  }
-		},
-		{
-		  "path": "pages/find_funds/fundsOrder",
-		  "style": {
-		    "navigationBarTitleText": "找款订单",
-		    "navigationBarBackgroundColor": "#ffe079",
-		    "navigationBarTextStyle": "black",
-		    "app-plus": {
-		      "titleNView": {
-		        "type": "default"
-		      }
-		    }
-		  }
-		},
-		{
-			"path": "pages/goods_search/index",
-			"style": {
-				"navigationBarBackgroundColor": "#FFE079",
-				"navigationBarTitleText": "搜索商品",
-				"navigationBarTextStyle": "black"
-			}
-		},
-		{
-			"path": "pages/goods_details/index",
-			"style": {
-				"navigationStyle": "custom",
-				"navigationBarTextStyle": "black"
-			}
-		},
+
 		{
 			"path": "pages/order_addcart/order_addcart",
 			"style": {
@@ -96,24 +56,6 @@
 				"navigationBarTitleText": "商品分类"
 			}
 		},
-		{
-			"path": "pages/personal_info/personal_info",
-			"style": {
-				"navigationBarTitleText": "个人资料",
-				"navigationBarBackgroundColor": "#ffe079",
-				"navigationBarTextStyle": "black",
-				"enablePullDownRefresh": false
-			}
-		},
-		{
-			"path": "pages/merchant/index",
-			"style": {
-				"navigationBarTitleText": "商家主页",
-				"navigationBarBackgroundColor": "#ffffff",
-				"navigationBarTextStyle": "black",
-				"enablePullDownRefresh": false
-			}
-		},
 		{
 			"path": "pages/order_list/index",
 			"style": {
@@ -145,12 +87,7 @@
 				"navigationStyle": "custom"
 			}
 		},
-		{
-			"path": "pages/VIP/VIP",
-			"style": {
-				"navigationBarTitleText": "VIP权益"
-			}
-		},
+
 		{
 			"path": "pages/webview/index",
 			"style": {
@@ -158,12 +95,6 @@
 				"navigationBarBackgroundColor": "#ffe079",
 				"navigationBarTextStyle": "black"
 			}
-		},
-		{
-			"path": "pages/SVIP/SVIP",
-			"style": {
-				"navigationBarTitleText": "SVIP权益"
-			}
 		}
 	],
 	"subPackages": [
@@ -331,6 +262,14 @@
 						}
 					}
 				},
+				{
+					"path": "vault/index",
+					"style": {
+						"navigationBarTitleText": "钱包",
+						"navigationBarBackgroundColor": "#ffe079",
+						"navigationBarTextStyle": "black"
+					}
+				},
 				{
 					"path": "user_goods_collection/index",
 					"style": {
@@ -338,6 +277,27 @@
 						"navigationBarBackgroundColor": "#ffe079",
 						"navigationBarTextStyle": "black"
 					}
+				},
+				{
+					"path": "VIP/VIP",
+					"style": {
+						"navigationBarTitleText": "VIP权益"
+					}
+				},
+				{
+					"path": "SVIP/SVIP",
+					"style": {
+						"navigationBarTitleText": "SVIP权益"
+					}
+				},
+				{
+					"path": "personal_info/personal_info",
+					"style": {
+						"navigationBarTitleText": "个人资料",
+						"navigationBarBackgroundColor": "#ffe079",
+						"navigationBarTextStyle": "black",
+						"enablePullDownRefresh": false
+					}
 				}
 			]
 		},
@@ -345,6 +305,15 @@
 			"root": "pages/merchantCenters",
 			"name": "merchantCenters",
 			"pages": [
+				{
+					"path": "merchant",
+					"style": {
+						"navigationBarTitleText": "商家主页",
+						"navigationBarBackgroundColor": "#ffffff",
+						"navigationBarTextStyle": "black",
+						"enablePullDownRefresh": false
+					}
+				},
 				{
 					"path": "postInformation",
 					"style": {
@@ -378,6 +347,27 @@
 					}
 				}
 			]
+		},
+		{
+			"root": "pages/goods",
+			"name": "goods",
+			"pages": [
+				{
+					"path": "goods_details/index",
+					"style": {
+						"navigationStyle": "custom",
+						"navigationBarTextStyle": "black"
+					}
+				},
+				{
+					"path": "goods_search/index",
+					"style": {
+						"navigationBarBackgroundColor": "#FFE079",
+						"navigationBarTitleText": "搜索商品",
+						"navigationBarTextStyle": "black"
+					}
+				}
+			]
 		}
 	],
 	"globalStyle": {

+ 0 - 312
pages/find_funds/findFunds.vue

@@ -1,312 +0,0 @@
-<template>
-  <view class="page-container">
-    <view class="banner"></view>
-    <view class="find-funds-info">
-      <view class="title-box">
-        <view class="title">请上传1~3张产品图</view>
-        <view class="f-title">图片越清晰越容易识别款式哦~</view>
-      </view>
-      <!-- 上传 -->
-      <view class="upload-box">
-        <up-upload
-          :fileList="imageList"
-          uploadIcon="plus"
-          @afterRead="afterRead"
-          @delete="deletePic"
-          name="1"
-          multiple
-          :maxCount="3"
-        >
-          <template #trigger>
-            <view class="upload-block">
-              <uni-icons size="38" color="#ccc" type="plusempty"></uni-icons>
-            </view>
-          </template>
-        </up-upload>
-      </view>
-      <!-- 信息 -->
-      <view class="info-box">
-        <view class="info-item">
-          <view class="info-label">预估重量(g)</view>
-          <view class="info-input">
-            <input type="number" v-model="findFunds.estimatedWeight" />
-          </view>
-        </view>
-        <view class="info-item">
-          <view class="info-label">理想工费(g)</view>
-          <view class="info-input">
-            <input type="number" v-model="findFunds.idealWage" />
-          </view>
-        </view>
-        <view class="info-item">
-          <view class="info-label">购买件数</view>
-          <view class="info-input">
-            <input type="number" v-model="findFunds.numberOfUnits" />
-          </view>
-        </view>
-        <view class="info-item">
-          <view class="info-label">金属类型</view>
-
-          <!-- <up-radio-group v-model="goldType" @change="selectGoldType">
-            <up-radio
-              v-for="item in goldList"
-              :key="item.title"
-              :name="item.name"
-              activeColor="#e9c279"
-              :label="item.title"
-            ></up-radio>
-          </up-radio-group> -->
-          <radio-group
-            @change="selectGoldType"
-            class="radio-box"
-            :value="goldType"
-          >
-            <label class="lable-box" v-for="item in goldList" :key="item.title">
-              <view>
-                <radio
-                  :value="item.value"
-                  activeBackgroundColor="#e9c279"
-                  :checked="goldType == item.value"
-                />
-              </view>
-              <view>{{ item.title }}</view>
-            </label>
-          </radio-group>
-        </view>
-        <view class="info-item">
-          <view class="info-label">买家留言</view>
-        </view>
-        <view class="info-input info-item-message">
-          <up-textarea
-            v-model="findFunds.description"
-            placeholder="请输入您的留言,例如:l件3克的戒指"
-            :maxlength="30"
-            :count="true"
-          ></up-textarea>
-        </view>
-      </view>
-    </view>
-    <!-- 发布找款 -->
-    <view class="btn-box" @click="publish">
-      <image class="sb-btn-img" src="/static/images/sb_btn.png"></image>
-      <text>发布找款</text>
-    </view>
-    <!-- 提示 -->
-    <view class="find-finish-tips">
-      *发布找款后,正常48小时内会有代购接单,如超时代表未找到该订单,
-      请关注订单状态。
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { useToast } from "@/hooks/useToast";
-import { useImageUpload } from "@/hooks/useImageUpload";
-import { ref, reactive } from "vue";
-import { findFundsAPI } from "@/api/find_fund";
-const { imageList, afterRead, deletePic, uploadLoading } = useImageUpload({
-  pid: 10,
-  model: "Refund",
-});
-const { Toast } = useToast();
-// 选择金料类型
-const goldList = reactive([
-  {
-    title: "黄金",
-    value: "au",
-  },
-
-  {
-    title: "白银",
-    value: "ag",
-  },
-  {
-    title: "铂金",
-    value: "pt",
-  },
-  // {
-  //   title: "K金",
-  //   name: "kau",
-  // },
-]);
-const goldType = ref("au");
-const selectGoldType = (e) => {
-  goldType.value = e.detail.value;
-};
-const findFunds = ref({
-  estimatedWeight: "",
-  idealWage: "",
-  metalType: "",
-  numberOfUnits: "",
-  purchaserImgs: [],
-  description: "",
-});
-// 发布找款
-const publish = async () => {
-  if (checkData()) {
-    const res = await findFundsAPI({
-      ...findFunds.value,
-      metalType: goldType.value,
-      purchaserImgs: imageList.value.map((v) => v.info.url),
-    });
-    Toast({ title: "发布成功", icon: "success" });
-    uni.redirectTo({ url: "/pages/find_funds/fundsOrder" });
-  }
-};
-const checkData = () => {
-  if (findFunds.value.estimatedWeight == "") {
-    Toast({ title: "请填写预估重量" });
-    return false;
-  }
-  if (findFunds.value.idealWage == "") {
-    Toast({ title: "请填写理想工费" });
-    return false;
-  }
-  if (findFunds.value.numberOfUnits == "") {
-    Toast({ title: "请填写购买件数" });
-    return false;
-  }
-  if (findFunds.value.description == "") {
-    Toast({ title: "请填写买家留言" });
-    return false;
-  }
-  if (imageList.value.length == 0) {
-    Toast({ title: "请上传产品图" });
-    return false;
-  }
-  return true;
-};
-// 请输入您的留言
-// const message = ref("");
-</script>
-
-<style lang="scss" scoped>
-.page-container {
-  width: 750rpx;
-  height: 100vh;
-  padding: 20rpx 30rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  position: relative;
-  min-height: 100vh;
-  background: $uni-bg-primary;
-
-  //   .banner {
-  //     position: absolute;
-  //     left: 0;
-  //     top: 0;
-  //     width: 100%;
-  //     height: 50vh;
-  //     background-image: linear-gradient(
-  //       360deg,
-  //       #ffffff 0%,
-  //       #e8c279 100%
-  //     ) !important;
-  //     z-index: -1;
-  //   }
-  .find-funds-info {
-    width: 682rpx;
-    min-height: 1069rpx;
-    background-color: #ffffff;
-    box-shadow: 0rpx 3rpx 13rpx 0rpx rgba(0, 0, 0, 0.13);
-    border-radius: 20rpx;
-    box-sizing: border-box;
-    padding: 40rpx;
-    .title-box {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      //   padding: 30rpx;
-      box-sizing: border-box;
-      .title {
-        font-size: 28rpx;
-        color: #000000;
-      }
-      .f-title {
-        font-size: 21rpx;
-        color: #848484;
-      }
-    }
-    .upload-box {
-      margin-top: 40rpx;
-      .upload-block {
-        width: 160rpx;
-        height: 160rpx;
-        border: 1px solid #ccc;
-        border-radius: 10rpx;
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        align-items: center;
-        color: #ccc;
-        font-weight: 700;
-        font-size: 26rpx;
-      }
-    }
-    .info-box {
-      .info-item {
-        display: flex;
-        align-items: center;
-        margin: 40rpx 0rpx;
-        position: relative;
-        .info-label {
-          font-size: 26rpx;
-          color: #000000;
-          width: 200rpx;
-        }
-        .info-input {
-          width: 437rpx;
-          height: 69rpx;
-          background-color: #f3f3f3;
-          border-radius: 10rpx;
-          display: flex;
-          align-items: center;
-          input {
-            width: 100%;
-            height: 100%;
-            padding-left: 10rpx;
-          }
-        }
-      }
-    }
-  }
-  .btn-box {
-    width: 270rpx;
-    height: 66rpx;
-    font-size: 28rpx;
-    color: #000;
-    position: relative;
-    margin-top: 40rpx;
-    .sb-btn-img {
-      width: 100%;
-      height: 100%;
-    }
-    text {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      transform: translate(-50%, -50%);
-      white-space: nowrap;
-    }
-  }
-  .find-finish-tips {
-    margin-top: 50rpx;
-    font-family: Source Han Sans CN;
-    font-size: 21rpx;
-    line-height: 30rpx;
-    color: #7c7c7c;
-  }
-  .radio-box {
-    width: 437rpx;
-    display: flex;
-    justify-content: space-between;
-    .lable-box {
-      display: flex;
-      align-items: center;
-
-      margin-right: 10rpx;
-    }
-  }
-}
-</style>

File diff suppressed because it is too large
+ 0 - 1262
pages/find_funds/fundsOrder.vue


+ 0 - 0
pages/find_funds/index.vue


+ 5 - 5
pages/goods_details/index.vue

@@ -417,7 +417,7 @@ import productWindow from "@/components/productWindow";
 import { isTabBarPage } from "@/utils/util.js";
 import $util from "@/utils/util.js";
 import { HTTP_REQUEST_URL_SHARE } from "@/config/app.js";
-import UniIcons from "../../uni_modules/uni-icons/components/uni-icons/uni-icons.vue";
+import UniIcons from "../../../uni_modules/uni-icons/components/uni-icons/uni-icons.vue";
 const { Toast } = useToast();
 
 const app = getApp();
@@ -985,7 +985,7 @@ const getQrcodeFn = async () => {
     const data = {
       pid: uid.value,
       id: id.value,
-      path: "pages/goods_details/index",
+      path: "pages/goods/goods_details/index",
     };
     const res = await getQrcode(data);
     base64src(res.data.code, (res) => {
@@ -1170,7 +1170,7 @@ const handleShare = () => {
   console.log("uni.getSystemInfoSync()", uni.getSystemInfoSync());
   if (uni.getSystemInfoSync().romName !== "HarmonyOS") {
     uni.shareWithSystem({
-      href: `${HTTP_REQUEST_URL_SHARE}/share/#/pages/goods_details/index?articleId=${id.value}`,
+      href: `${HTTP_REQUEST_URL_SHARE}/share/#/pages/goods/goods_details/index?articleId=${id.value}`,
       title: productInfo.value.storeName,
       success() {
         // 分享完成,请注意此时不一定是成功分享
@@ -1184,7 +1184,7 @@ const handleShare = () => {
       provider: "weixin",
       scene: "WXSceneSession",
       type: 0,
-      href: `${HTTP_REQUEST_URL_SHARE}/share/#/pages/goods_details/index?articleId=${id.value}`,
+      href: `${HTTP_REQUEST_URL_SHARE}/share/#/pages/goods/goods_details/index?articleId=${id.value}`,
       title: productInfo.value.storeName,
       imageUrl:
         "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/crmebimage/public/maintain/2025/08/28/eb3953f78a0f468fa2e4c7721c2a6ca2b9pirw5y37.png",
@@ -1291,7 +1291,7 @@ const metalTypeFormatter = (val)=> {
   return str;
 }
 const toStore = () => {
-  uni.navigateTo({ url: "/pages/merchant/index?merchantId="+sbMerchantInfo.value.id });
+  uni.navigateTo({ url: "/pages/merchantCenters/merchant?merchantId="+sbMerchantInfo.value.id });
 }
 /**
  *

+ 0 - 215
pages/goods_search/index.vue

@@ -1,215 +0,0 @@
-<template>
-  <view>
-    <view class='searchGood'>
-      <view class='search acea-row row-between-wrapper'>
-        <view class='input acea-row row-between-wrapper'>
-          <text class='iconfont icon-sousuo'></text>
-          <input type='text' confirm-type="search" :value='searchValue' :focus="focus" placeholder='点击搜索商品' placeholder-class='placeholder'
-            @input="setValue" @confirm="searchBut" />
-        </view>
-        <view class='bnt' @tap='searchBut'>搜索</view>
-      </view>
-      <view class='title'>热门搜索</view>
-      <view class='list acea-row'>
-        <block v-for="(item, index) in hotSearchList" :key="index">
-          <view class='item' @tap='setHotSearchValue(item.title)'>{{ item.title }}</view>
-        </block>
-      </view>
-      <view class='line'></view>
-      <goodList :bastList="bastList" v-if="bastList.length > 0"></goodList>
-      <view class='loadingicon acea-row row-center-wrapper' v-if="bastList.length > 0">
-        <text class='loading iconfont icon-jiazai' :hidden='loading == false'></text>{{ loadTitle }}
-      </view>
-    </view>
-    <view class='noCommodity'>
-      <view class='pictrue' v-if="bastList.length == 0 && isbastList">
-        <image :src="HTTP_REQUEST_URL_IMG+'noSearch.png'"></image>
-      </view>
-      <recommend :hostProduct='hostProduct' v-if="bastList.length == 0"></recommend>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-import { onShow, onReachBottom } from '@dcloudio/uni-app'
-import { getSearchKeyword, getProductslist, getProductHot } from '@/api/store.js'
-import goodList from '@/components/goodList'
-import recommend from '@/components/recommend'
-import { HTTP_REQUEST_URL_IMG } from "@/config/app";
-
-// 响应式数据
-const hostProduct = ref([])
-const searchValue = ref('')
-const focus = ref(true)
-const bastList = ref([])
-const hotSearchList = ref([])
-const limit = ref(8)
-const page = ref(1)
-const loading = ref(false)
-const loadend = ref(false)
-const loadTitle = ref('加载更多')
-const hotPage = ref(1)
-const isScroll = ref(true)
-const isbastList = ref(false)
-
-// 获取热搜
-function getRoutineHotSearch() {
-  getSearchKeyword().then(res => {
-    hotSearchList.value = res.data
-  })
-}
-
-// 获取商品列表
-function getProductList() {
-  if (loadend.value || loading.value) return
-  loading.value = true
-  loadTitle.value = ''
-  getProductslist({
-    keyword: searchValue.value,
-    page: page.value,
-    limit: limit.value
-  }).then(res => {
-    const list = res.data.list
-    const isLoadend = list.length < limit.value
-    // 合并数组
-    bastList.value = (bastList.value || []).concat(list)
-    loading.value = false
-    loadend.value = isLoadend
-    loadTitle.value = isLoadend ? "😕人家是有底线的~~" : "加载更多"
-    page.value += 1
-    isbastList.value = true
-  }).catch(() => {
-    loading.value = false
-    loadTitle.value = '加载更多'
-  })
-}
-
-// 获取热门商品
-function getHostProduct() {
-  if (!isScroll.value) return
-  getProductHot(hotPage.value, limit.value).then(res => {
-    isScroll.value = res.data.list.length >= limit.value
-    hostProduct.value = hostProduct.value.concat(res.data.list)
-    hotPage.value += 1
-  })
-}
-
-// 设置热搜值
-function setHotSearchValue(val) {
-  searchValue.value = val
-  page.value = 1
-  loadend.value = false
-  bastList.value = []
-  getProductList()
-}
-
-// 输入框赋值
-function setValue(event) {
-  searchValue.value = event.detail.value
-}
-
-// 搜索按钮
-function searchBut() {
-  focus.value = false
-  if (searchValue.value.length > 0) {
-    page.value = 1
-    loadend.value = false
-    bastList.value = []
-    uni.showLoading({ title: '正在搜索中' })
-    getProductList()
-    uni.hideLoading()
-  } else {
-    // 这里假设 $util.Tips 已全局挂载
-    uni.$u.toast('请输入要搜索的商品')
-  }
-}
-
-// 生命周期
-onShow(() => {
-  getRoutineHotSearch()
-  getHostProduct()
-})
-
-onReachBottom(() => {
-  if (bastList.value.length > 0) {
-    getProductList()
-  } else {
-    getHostProduct()
-  }
-})
-</script>
-
-<style lang="scss">
-page {
-  margin-top: var(--status-bar-height);
-  background-color: #fff !important;
-}
-
-.searchGood .search {
-  padding-left: 30rpx;
-  background-color: #fff !important;
-}
-
-.searchGood .search {
-  padding-top: 20rpx;
-}
-
-.searchGood .search .input {
-  width: 598rpx;
-  background-color: #f7f7f7;
-  border-radius: 33rpx;
-  padding: 0 35rpx;
-  box-sizing: border-box;
-  height: 66rpx;
-}
-
-.searchGood .search .input input {
-  width: 472rpx;
-  font-size: 26rpx;
-}
-
-.searchGood .search .input .placeholder {
-  color: #bbb;
-}
-
-.searchGood .search .input .iconfont {
-  color: #000;
-  font-size: 35rpx;
-}
-
-.searchGood .search .bnt {
-  width: 120rpx;
-  text-align: center;
-  height: 66rpx;
-  line-height: 66rpx;
-  font-size: 30rpx;
-  color: #282828;
-}
-
-.searchGood .title {
-  font-size: 28rpx;
-  color: #999;
-  margin: 50rpx 30rpx 25rpx 30rpx;
-}
-
-.searchGood .list {
-  padding-left: 10rpx;
-}
-
-.searchGood .list .item {
-  font-size: 26rpx;
-  color: #454545;
-  padding: 0 21rpx;
-  height: 60rpx;
-  border-radius: 30rpx;
-  line-height: 60rpx;
-  border: 1rpx solid #aaa;
-  margin: 0 0 20rpx 20rpx;
-}
-
-.searchGood .line {
-  border-bottom: 1rpx solid #eee;
-  margin: 20rpx 30rpx 0 30rpx;
-}
-</style>

+ 2 - 2
pages/index/index.vue

@@ -447,7 +447,7 @@ onReachBottom(() => {
 // Product Details
 const goDetail = async (item) => {
   await goShopDetail(item, uid);
-  uni.navigateTo({ url: `/pages/goods_details/index?id=${item.id}` });
+  uni.navigateTo({ url: `/pages/goods/goods_details/index?id=${item.id}` });
 };
 function getAllCategory () {
   getCategoryList().then(res => {
@@ -458,7 +458,7 @@ const toBrowsingHistory = () => {
   uni.navigateTo({ url: `/pages/users/browsing_history/index?merchantId=${merchantId.value}` });
 }
 const toMerchant = (merchantId) => {
-  uni.navigateTo({ url:"/pages/merchant/index?merchantId="+merchantId });
+  uni.navigateTo({ url:"/pages/merchantCenters/merchant?merchantId="+merchantId });
 }
 </script>
 

+ 1 - 1
pages/merchantCenter/index.vue

@@ -206,7 +206,7 @@ const navigateTo = (url) => {
 // 用户面板事件处理
 function handleEdit() {
   // console.log("编辑资料");
-  uni.navigateTo({ url: "/pages/personal_info/personal_info" });
+  uni.navigateTo({ url: "/pages/users/personal_info/personal_info" });
 }
 // 手机号隐藏中间4位
 function hidePhoneNumber(phone) {

pages/merchant/index.vue → pages/merchantCenters/merchant.vue


+ 1 - 1
pages/order_addcart/order_addcart.vue

@@ -58,7 +58,7 @@
 
                         <navigator
                             :render-link="false"
-                            :url="'/pages/goods_details/index?id=' + item.productId"
+                            :url="'/pages/goods/goods_details/index?id=' + item.productId"
                             hover-class="none"
                             class="picTxt row-between-wrapper"
                         >

+ 3 - 3
pages/user/index.vue

@@ -268,7 +268,7 @@ const openVip = () => {
 
 // 查看会员权益
 const goVIP = ()=>{
-	uni.navigateTo({ url:"/pages/VIP/VIP" });
+	uni.navigateTo({ url:"/pages/users/VIP/VIP" });
 }
 
 // 查看交易明细
@@ -348,7 +348,7 @@ const viewStore = (store) => {
   if(!store.merchantId ){
     return;
   }
-  uni.navigateTo({ url:"/pages/merchant/index?merchantId="+store.merchantId });
+  uni.navigateTo({ url:"/pages/merchantCenters/merchant?merchantId="+store.merchantId });
 }
 
 // 查看全部订单
@@ -367,7 +367,7 @@ const navigateTo = (url) => {
 // 用户面板事件处理
 function handleEdit() {
   // console.log("编辑资料");
-  uni.navigateTo({ url: "/pages/personal_info/personal_info" });
+  uni.navigateTo({ url: "/pages/users/personal_info/personal_info" });
 }
 async function fetchMetalBalance() {
   // 若appStore.uid为空,则请求失败

pages/SVIP/SVIP.vue → pages/users/SVIP/SVIP.vue


+ 2 - 2
pages/VIP/VIP.vue

@@ -193,7 +193,7 @@ const taskList = ref([
     name: "完善基本资料",
     reward: "获得200成长值,200贝币",
     btnText: "立即前往",
-    url: "/pages/personal_info/personal_info",
+    url: "/pages/users/personal_info/personal_info",
   },
   // {
   //   name: "每日签到",
@@ -236,7 +236,7 @@ const taskList = ref([
     name: "升级SVIP",
     reward: "升级SVIP解锁更多功能,加速成长值成长",
     btnText: "立即前往",
-    url: "/pages/SVIP/SVIP",
+    url: "/pages/users/SVIP/SVIP",
   },
 ]);
 

+ 1 - 1
pages/users/browsing_history/index.vue

@@ -73,7 +73,7 @@ const getList = async () => {
   }
 };
 const toMerchant = (obj) => {
-  uni.navigateTo({ url:"/pages/merchant/index?merchantId="+obj.merchantId });
+  uni.navigateTo({ url:"/pages/merchantCenters/merchant?merchantId="+obj.merchantId });
 }
 </script>
 

+ 248 - 235
pages/users/order_confirm/index.vue

@@ -2,24 +2,24 @@
   <view>
     <view class="order-submission">
       <view
-        class="allAddress"
-        :style="store_self_mention ? '' : 'padding-top:10rpx;'"
+          class="allAddress"
+          :style="store_self_mention ? '' : 'padding-top:10rpx;'"
       >
         <view class="nav acea-row">
           <view
-            class="item font-color"
-            :class="shippingType == 0 ? 'on' : 'on2'"
-            @tap="addressType(0)"
-            v-if="store_self_mention"
+              class="item font-color"
+              :class="shippingType == 0 ? 'on' : 'on2'"
+              @tap="addressType(0)"
+              v-if="store_self_mention"
           ></view>
           <!-- <view class="item font-color" :class="shippingType == 1 ? 'on' : 'on2'" @tap="addressType(1)"
 						v-if="store_self_mention"></view> -->
         </view>
         <view
-          class="address acea-row row-between-wrapper"
-          @tap="onAddress"
-          v-if="shippingType == 0"
-          :style="
+            class="address acea-row row-between-wrapper"
+            @tap="onAddress"
+            v-if="shippingType == 0"
+            :style="
             store_self_mention
               ? ''
               : 'border-top-left-radius: 14rpx;border-top-right-radius: 14rpx;'
@@ -27,15 +27,15 @@
         >
           <view class="addressCon" v-if="addressInfo.realName">
             <view class="name"
-              >{{ addressInfo.realName }}
+            >{{ addressInfo.realName }}
               <text class="phone">{{ addressInfo.phone }}</text>
             </view>
             <view class="acea-row">
               <text class="default font-color" v-if="addressInfo.isDefault"
-                >[默认]</text
+              >[默认]</text
               >
               <text class="line2"
-                >{{ addressInfo.province }}{{ addressInfo.city
+              >{{ addressInfo.province }}{{ addressInfo.city
                 }}{{ addressInfo.district }}{{ addressInfo.detail }}</text
               >
             </view>
@@ -46,14 +46,14 @@
           <view class="iconfont icon-jiantou"></view>
         </view>
         <view
-          class="address acea-row row-between-wrapper"
-          v-else
-          @tap="showStoreList"
+            class="address acea-row row-between-wrapper"
+            v-else
+            @tap="showStoreList"
         >
           <block v-if="storeList.length > 0">
             <view class="addressCon">
               <view class="name"
-                >{{ system_store.name }}
+              >{{ system_store.name }}
                 <text class="phone">{{ system_store.phone }}</text>
               </view>
               <view class="line1">
@@ -73,19 +73,19 @@
       </view>
       <view class="pad30">
         <orderGoods
-          :mallType="orderInfoVo.mallType"
-          :cartInfo="cartInfo"
-          :orderProNum="orderProNum"
+            :mallType="orderInfoVo.mallType"
+            :cartInfo="cartInfo"
+            :orderProNum="orderProNum"
         ></orderGoods>
         <view class="wrapper borRadius14">
           <view
-            class="item acea-row row-between-wrapper"
-            @tap="couponTap"
-            v-if="orderInfoVo.mallType === 0"
+              class="item acea-row row-between-wrapper"
+              @tap="couponTap"
+              v-if="orderInfoVo.mallType === 0"
           >
             <view>优惠券</view>
             <view class="discount"
-              >{{ couponTitle }}
+            >{{ couponTitle }}
               <text class="iconfont icon-jiantou"></text>
             </view>
           </view>
@@ -93,8 +93,8 @@
             <view>快递费用</view>
             <block v-if="orderInfoVo.mallType === 0">
               <view
-                class="discount"
-                v-if="
+                  class="discount"
+                  v-if="
                   parseFloat(orderInfoVo.freightFee) > 0 &&
                   orderInfoVo.mallType === 0
                 "
@@ -108,12 +108,12 @@
           <view class="item" v-if="textareaStatus">
             <view>备注信息</view>
             <textarea
-              v-if="coupon.coupon === false"
-              placeholder-class="placeholder"
-              @input="bindHideKeyboard"
-              value=""
-              name="mark"
-              placeholder="请添加备注(150字以内)"
+                v-if="coupon.coupon === false"
+                placeholder-class="placeholder"
+                @input="bindHideKeyboard"
+                value=""
+                name="mark"
+                placeholder="请添加备注(150字以内)"
             ></textarea>
           </view>
         </view>
@@ -130,7 +130,11 @@
                 <view class="iconfont icon-touzijintiao1"></view>
                 <view class="lable-icon">
                   <view class="lable">余料支付</view>
-                  <view class="value"> 可用余料:{{ auBalance }}</view>
+                  <view class="value">
+                    可用余料:
+                    {{ metalTypeMap[metalType].name }}
+                    :{{ metalTypeMap[metalType].balance || 0 }}g
+                  </view>
                 </view>
               </view>
               <!-- <switch
@@ -144,16 +148,16 @@
             </view>
             <view class="list">
               <view
-                v-for="(item, index) in avaliablePayments"
-                class="payItem acea-row row-middle"
-                :class="active == index ? 'on' : ''"
-                @tap="payItem(index)"
-                :key="index"
+                  v-for="(item, index) in avaliablePayments"
+                  class="payItem acea-row row-middle"
+                  :class="active == index ? 'on' : ''"
+                  @tap="payItem(index)"
+                  :key="index"
               >
                 <view class="name acea-row row-center-wrapper">
                   <view
-                    class="iconfont animated"
-                    :class="
+                      class="iconfont animated"
+                      :class="
                       item.icon +
                       ' ' +
                       (animated == true && active == index ? 'bounceIn' : '')
@@ -178,8 +182,8 @@
             </view>
           </view>
           <view
-            class="item acea-row row-between-wrapper"
-            v-if="orderInfoVo.couponFee > 0"
+              class="item acea-row row-between-wrapper"
+              v-if="orderInfoVo.couponFee > 0"
           >
             <view>优惠券抵扣:</view>
             <view class="money">-¥{{ orderInfoVo.couponFee }}</view>
@@ -193,39 +197,39 @@
           </view> -->
 
           <view
-            class="item acea-row row-between-wrapper"
-            v-if="orderInfoVo.mallType === 0"
+              class="item acea-row row-between-wrapper"
+              v-if="orderInfoVo.mallType === 0"
           >
             <view>工费:</view>
             <view class="money">+¥{{ orderInfoVo.totalLaborCost }}</view>
           </view>
 
           <view
-            class="item acea-row row-between-wrapper"
-            v-if="
+              class="item acea-row row-between-wrapper"
+              v-if="
               orderInfoVo.mallType === 0 && orderInfoVo?.totalAdditionalAmount
             "
           >
             <view>附加费:</view>
             <view class="money"
-              >+¥{{ orderInfoVo.totalAdditionalAmount }}</view
+            >+¥{{ orderInfoVo.totalAdditionalAmount }}</view
             >
           </view>
 
           <view
-            class="item acea-row row-between-wrapper"
-            v-if="orderInfoVo.freightFee > 0"
+              class="item acea-row row-between-wrapper"
+              v-if="orderInfoVo.freightFee > 0"
           >
             <view>运费:</view>
             <view class="money">+¥{{ orderInfoVo.freightFee }}</view>
           </view>
 
           <view
-            v-if="
+              v-if="
               orderInfoVo.mallType === 0 &&
               Number(orderInfoVo.vipLevelDis) !== 0
             "
-            class="item acea-row row-between-wrapper"
+              class="item acea-row row-between-wrapper"
           >
             <view>会员折扣:</view>
             <view class="money">-¥{{ orderInfoVo.vipLevelDis }}</view>
@@ -249,31 +253,31 @@
       </view>
     </view>
     <couponListWindow
-      :coupon="coupon"
-      @close="closeCouponPopup"
-      :showPopup="showCouponPopup"
-      :openType="openType"
-      @ChangCoupons="ChangCoupons"
-      :orderShow="orderShow"
+        :coupon="coupon"
+        @close="closeCouponPopup"
+        :showPopup="showCouponPopup"
+        :openType="openType"
+        @ChangCoupons="ChangCoupons"
+        :orderShow="orderShow"
     ></couponListWindow>
     <addressWindow
-      ref="addressWindowRef"
-      @changeTextareaStatus="changeTextareaStatus"
-      :address="address"
-      :pagesUrl="pagesUrl"
-      @OnDefaultAddress="OnDefaultAddress"
-      @OnChangeAddress="OnChangeAddress"
-      @changeClose="changeClose"
+        ref="addressWindowRef"
+        @changeTextareaStatus="changeTextareaStatus"
+        :address="address"
+        :pagesUrl="pagesUrl"
+        @OnDefaultAddress="OnDefaultAddress"
+        @OnChangeAddress="OnChangeAddress"
+        @changeClose="changeClose"
     />
 
     <!-- 余额不足modal -->
     <up-modal
-      :showCancelButton="true"
-      :show="showModal"
-      title="余额不足"
-      confirmText="去充值"
-      @cancel="showModal = false"
-      @confirm="modalConfirm"
+        :showCancelButton="true"
+        :show="showModal"
+        title="余额不足"
+        confirmText="去充值"
+        @cancel="showModal = false"
+        @confirm="modalConfirm"
     ></up-modal>
   </view>
 </template>
@@ -331,24 +335,24 @@ const textareaStatus = ref(true);
 
 // 响应式变量:支付方式列表(包含支付宝、微信、余额等支付方式配置)
 const payments = ref([
-  {
-    name: "支付宝支付", // 支付方式名称
-    icon: "icon-zhifubao", // 图标类名
-    value: "alipay", // 支付标识值
-    title: "支付宝快捷支付", // 支付说明
-    payType: "alipay", // 支付类型
-    payStatus: 1, // 支付状态(1-可用,0-不可用)
-    payChannel: "appAliPay",
-  },
-  {
-    name: "微信支付",
-    icon: "icon-wechat",
-    value: "weixin",
-    title: "微信快捷支付",
-    payType: "weixin",
-    payStatus: 1,
-    payChannel: "weixinApp",
-  },
+  // {
+  //   name: "支付宝支付", // 支付方式名称
+  //   icon: "icon-zhifubao", // 图标类名
+  //   value: "alipay", // 支付标识值
+  //   title: "支付宝快捷支付", // 支付说明
+  //   payType: "alipay", // 支付类型
+  //   payStatus: 0, // 支付状态(1-可用,0-不可用)
+  //   payChannel: "appAliPay",
+  // },
+  // {
+  //   name: "微信支付",
+  //   icon: "icon-wechat",
+  //   value: "weixin",
+  //   title: "微信快捷支付",
+  //   payType: "weixin",
+  //   payStatus: 0,
+  //   payChannel: "weixinApp",
+  // },
   {
     name: "余额支付",
     icon: "icon-balance",
@@ -358,21 +362,23 @@ const payments = ref([
     payStatus: 1,
     payChannel: "weixinh5",
   },
-  {
-    name: "贝币支付",
-    icon: "icon-balance",
-    value: "yue",
-    title: "可用贝币:",
-    payType: "yue",
-    payStatus: 0,
-    payChannel: "weixinh5",
-  },
+  // {
+  //   name: "贝币支付",
+  //   icon: "icon-balance",
+  //   value: "yue",
+  //   title: "可用贝币:",
+  //   payType: "yue",
+  //   payStatus: 0,
+  //   payChannel: "weixinh5",
+  // },
 ]);
 
 // 计算属性:过滤出可用的支付方式(只保留payStatus为1的项)
 const avaliablePayments = computed(() => {
+  console.log(payments.value)
   return payments.value.filter((item) => item.payStatus === 1);
 });
+console.log(avaliablePayments)
 
 // 响应式变量:当前选中的支付方式(默认余额支付)
 const payType = ref("alipay");
@@ -435,16 +441,16 @@ const userSelectPayTypeItem = ref(payments.value[0]);
 
 // 监听 isLogin
 watch(
-  () => appStore.isLogin, // 监听的数据源:全局登录状态
-  (newV) => {
-    // newV是登录状态的新值(true/false)
-    if (newV) {
-      // 若已登录
-      getloadPreOrder(); // 加载预订单信息
-      this.getaddressInfo(); // 获取地址信息(this指向当前组件实例)
-    }
-  },
-  { deep: true } // 深度监听(确保复杂数据类型变化也能触发)
+    () => appStore.isLogin, // 监听的数据源:全局登录状态
+    (newV) => {
+      // newV是登录状态的新值(true/false)
+      if (newV) {
+        // 若已登录
+        getloadPreOrder(); // 加载预订单信息
+        this.getaddressInfo(); // 获取地址信息(this指向当前组件实例)
+      }
+    },
+    { deep: true } // 深度监听(确保复杂数据类型变化也能触发)
 );
 // 商品总价
 
@@ -504,75 +510,82 @@ onMounted(() => {
 const useGoldPay = (e) => {
   computedPrice();
 };
-// 黄金余料
-const auBalance = ref(0);
+const metalTypeMap = {
+  1: { name: "黄金", balance: 0 },
+  2: { name: "铂金", balance: 0 },
+  3: { name: "白银", balance: 0 },
+};
+const metalType = ref(1);
+
 // 加载预订单信息(核心函数:初始化订单数据)
 const getloadPreOrder = () => {
   console.log("preOrderNo.value", preOrderNo.value);
 
   // 调用加载预订单API,传入预订单号
   loadPreOrderApi(preOrderNo.value)
-    .then((res) => {
-      console.log("getOrderDetail=========>", res.data);
-      auBalance.value = res.data.auBalance;
-      // 接口成功回调
-      let orderInfoVoData = res.data.orderInfoVo; // 订单基本信息
-      orderInfoVo.value = orderInfoVoData; // 保存订单信息
-      cartInfo.value = orderInfoVoData.orderDetailList; // 保存商品列表
-
-      orderProNum.value = orderInfoVoData.orderProNum; // 保存商品总数
-
-      // 设置地址ID(优先用参数传入的,没有则用订单默认的)
-      address.value.addressId = addressId.value
-        ? addressId.value
-        : orderInfoVoData.addressId;
-
-      // 判断是否支持门店自提(根据接口返回和全局状态)
-      store_self_mention.value =
-        res.data.storeSelfMention == "true" && // 接口返回支持自提
-        appStore.productTypeComputed === "normal" // 全局状态为普通商品
-          ? true
-          : false;
-
-      // 根据商城类型设置是否使用积分(mallType=1时默认使用)
-      useIntegral.value = orderInfoVo.value.mallType === 1;
-      // 根据商城类型设置默认支付方式(mallType=1时用贝币支付)
-      payType.value = orderInfoVo.value.mallType === 1 ? "yue" : "alipay";
-
-      // 动态更新支付方式状态(根据接口返回的配置)
-      payments.value.forEach((item) => {
-        if (item.name === "余额支付") {
-          // 余额支付:显示可用余额,状态由接口返回的yuePayStatus决定
-          item.title = "可用余额: " + orderInfoVoData.userBalance;
-          item.payStatus = parseInt(res.data.yuePayStatus) === 1 ? 1 : 2;
-        } else if (item.payType === "weixin") {
-          // 微信支付:状态由接口返回的payWeixinOpen决定
-          item.payStatus = parseInt(res.data.payWeixinOpen) === 1 ? 1 : 0;
-        }
+      .then((res) => {
+        metalTypeMap[1].balance = res.data.auBalance || 0;
+        metalTypeMap[2].balance = res.data.ptBalance || 0;
+        metalTypeMap[3].balance = res.data.agBalance || 0;
+        metalType.value = res.data.metalType;
+        // 接口成功回调
+        let orderInfoVoData = res.data.orderInfoVo; // 订单基本信息
+        orderInfoVo.value = orderInfoVoData; // 保存订单信息
+        cartInfo.value = orderInfoVoData.orderDetailList; // 保存商品列表
+
+        orderProNum.value = orderInfoVoData.orderProNum; // 保存商品总数
+
+        // 设置地址ID(优先用参数传入的,没有则用订单默认的)
+        address.value.addressId = addressId.value
+            ? addressId.value
+            : orderInfoVoData.addressId;
+
+        // 判断是否支持门店自提(根据接口返回和全局状态)
+        store_self_mention.value =
+            res.data.storeSelfMention == "true" && // 接口返回支持自提
+            appStore.productTypeComputed === "normal" // 全局状态为普通商品
+                ? true
+                : false;
+
+        // 根据商城类型设置是否使用积分(mallType=1时默认使用)
+        useIntegral.value = orderInfoVo.value.mallType === 1;
+        // 根据商城类型设置默认支付方式(mallType=1时用贝币支付)
+        payType.value = orderInfoVo.value.mallType === 1 ? "yue" : "alipay";
+
+        // 动态更新支付方式状态(根据接口返回的配置)
+        payments.value.forEach((item) => {
+          if (item.name === "余额支付") {
+            // 余额支付:显示可用余额,状态由接口返回的yuePayStatus决定
+            item.title = "可用余额: " + orderInfoVoData.userBalance;
+            item.payStatus = parseInt(res.data.yuePayStatus) === 1 ? 1 : 2;
+          } else if (item.payType === "weixin") {
+            // 微信支付:状态由接口返回的payWeixinOpen决定
+            item.payStatus = parseInt(res.data.payWeixinOpen) === 1 ? 1 : 0;
+          }
 
-        // 贝币支付:仅在mallType=1时可用
-        if (orderInfoVo.value.mallType === 1) {
-          item.title = "可用贝币: " + orderInfoVoData.userIntegral;
-          item.payStatus = item.name === "贝币支付" ? 1 : 0;
-        } else {
-          item.payStatus = item.name === "贝币支付" ? 0 : 1;
-        }
-      });
+          // 贝币支付:仅在mallType=1时可用
+          if (orderInfoVo.value.mallType === 1) {
+            item.title = "可用贝币: " + orderInfoVoData.userIntegral;
+            item.payStatus = item.name === "贝币支付" ? 1 : 0;
+          } else {
+            item.payStatus = item.name === "贝币支付" ? 0 : 1;
+          }
+        });
 
-      if (addressId.value) {
-        computedPrice();
-      }
-      // 调用子页面方法授权后执行获取地址列表
-      nextTick(() => {
-        addressWindowRef.value.fetchAddressList();
+        if (addressId.value) {
+          computedPrice();
+        }
+        // 调用子页面方法授权后执行获取地址列表
+        nextTick(() => {
+          addressWindowRef.value.fetchAddressList();
+        });
+      })
+      .catch((err) => {
+        // 接口失败回调
+        console.error(err); // 打印错误
+        uni.navigateTo({ url: "/pages/order_list/index" }); // 跳转到订单列表
+        Toast({ title: err }); // 提示错误信息
       });
-    })
-    .catch((err) => {
-      // 接口失败回调
-      console.error(err); // 打印错误
-      uni.navigateTo({ url: "/pages/order_list/index" }); // 跳转到订单列表
-      Toast({ title: err }); // 提示错误信息
-    });
 };
 // 授权回调事件(用户授权后触发,预留)
 const onLoadFun = () => {
@@ -595,16 +608,16 @@ const getList = () => {
   };
   // 调用门店列表API
   storeListApi(data)
-    .then((res) => {
-      // 成功回调
-      let list = res.data.list || []; // 门店列表
-      storeList.value = list; // 保存门店列表
-      system_store.value = list[0]; // 默认选中第一个门店
-    })
-    .catch((err) => {
-      // 失败回调
-      Toast({ title: err }); // 提示错误
-    });
+      .then((res) => {
+        // 成功回调
+        let list = res.data.list || []; // 门店列表
+        storeList.value = list; // 保存门店列表
+        system_store.value = list[0]; // 默认选中第一个门店
+      })
+      .catch((err) => {
+        // 失败回调
+        Toast({ title: err }); // 提示错误
+      });
 };
 
 // 关闭地址弹窗
@@ -634,24 +647,24 @@ function computedPrice() {
     preOrderNo: preOrderNo.value, // 预订单号
     goldNum: appStore.userInfo.goldBalance, // 余料数量
   })
-    .then((res) => {
-      // 成功回调
-      let data = res.data; // 计算后的价格数据
-      // 更新订单费用信息
-      orderInfoVo.value.couponFee = data.couponFee; // 优惠券抵扣
-      orderInfoVo.value.userIntegral = data.surplusIntegral; // 剩余积分
-      orderInfoVo.value.deductionPrice = data.deductionPrice; // 积分抵扣金额
-      orderInfoVo.value.freightFee = data.freightFee; // 运费
-      orderInfoVo.value.payFee = data.payFee; // 实付金额
-      orderInfoVo.value.proTotalFee = data.proTotalFee; // 商品总价
-      orderInfoVo.value.useIntegral = data.useIntegral; // 是否使用积分
-      orderInfoVo.value.usedIntegral = data.usedIntegral; // 已用积分
-      orderInfoVo.value.surplusIntegral = data.surplusIntegral; // 剩余积分
-    })
-    .catch((err) => {
-      // 失败回调
-      Toast({ title: err }); // 提示错误
-    });
+      .then((res) => {
+        // 成功回调
+        let data = res.data; // 计算后的价格数据
+        // 更新订单费用信息
+        orderInfoVo.value.couponFee = data.couponFee; // 优惠券抵扣
+        orderInfoVo.value.userIntegral = data.surplusIntegral; // 剩余积分
+        orderInfoVo.value.deductionPrice = data.deductionPrice; // 积分抵扣金额
+        orderInfoVo.value.freightFee = data.freightFee; // 运费
+        orderInfoVo.value.payFee = data.payFee; // 实付金额
+        orderInfoVo.value.proTotalFee = data.proTotalFee; // 商品总价
+        orderInfoVo.value.useIntegral = data.useIntegral; // 是否使用积分
+        orderInfoVo.value.usedIntegral = data.usedIntegral; // 已用积分
+        orderInfoVo.value.surplusIntegral = data.surplusIntegral; // 剩余积分
+      })
+      .catch((err) => {
+        // 失败回调
+        Toast({ title: err }); // 提示错误
+      });
 }
 
 // 切换配送方式(快递/自提)
@@ -695,9 +708,9 @@ const ChangCoupons = (e) => {
   // this.usableCoupon = e
   // this.coupon.coupon = false
   let index = e,
-    list = coupon.value.list,
-    couponTitleValue = "请选择",
-    couponIdValue = 0;
+      list = coupon.value.list,
+      couponTitleValue = "请选择",
+      couponIdValue = 0;
   console.log("list", list);
   for (let i = 0, len = list.length; i < len; i++) {
     if (i != index) {
@@ -821,7 +834,7 @@ const onAddress = () => {
   address.value.address = true; // 显示地址弹窗
   // 设置地址页面跳转链接(携带预订单号)
   pagesUrl.value =
-    "/pages/users/user_address_list/index?preOrderNo=" + preOrderNo.value;
+      "/pages/users/user_address_list/index?preOrderNo=" + preOrderNo.value;
 };
 
 // 联系人输入事件(保存联系人)
@@ -838,17 +851,17 @@ const phone = (e) => {
 const payment = (data) => {
   // 调用创建订单API
   orderCreate(data)
-    .then((res) => {
-      // 订单创建成功
-      // 调用支付流程,传入订单号和成功提示
-      getOrderPay(res.data.orderNo, "支付成功");
-    })
-    .catch((err) => {
-      // 订单创建失败
-      console.error("payment error", err); // 打印错误
-      uni.hideLoading(); // 隐藏加载中
-      Toast({ title: err }); // 提示错误
-    });
+      .then((res) => {
+        // 订单创建成功
+        // 调用支付流程,传入订单号和成功提示
+        getOrderPay(res.data.orderNo, "支付成功");
+      })
+      .catch((err) => {
+        // 订单创建失败
+        console.error("payment error", err); // 打印错误
+        uni.hideLoading(); // 隐藏加载中
+        Toast({ title: err }); // 提示错误
+      });
 };
 
 // 处理支付流程(根据支付类型发起支付并处理结果)
@@ -890,23 +903,23 @@ const getOrderPay = async (orderNo, message) => {
           console.log("alipayPaymentResult", res);
           // 提示成功并跳转到结果页
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.SUCCESS, icon: "success" },
-            { tab: 4, url: goPages }
+              { title: paymentConfig.PAYMENT_STATUS.SUCCESS, icon: "success" },
+              { tab: 4, url: goPages }
           );
         } else if (result.status === paymentConfig.PAYMENT_STATUS.FAIL) {
           // 支付失败:提示并跳转
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.FAIL },
-            { tab: 5, url: `${goPages}&msg=${failMsg}` }
+              { title: paymentConfig.PAYMENT_STATUS.FAIL },
+              { tab: 5, url: `${goPages}&msg=${failMsg}` }
           );
         } else if (result.status === paymentConfig.PAYMENT_STATUS.CANCEL) {
           // 支付取消:提示并跳转
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.CANCEL },
-            {
-              tab: 5,
-              url: `${goPages}&msg=${paymentConfig.PAYMENT_STATUS.CANCEL}`,
-            }
+              { title: paymentConfig.PAYMENT_STATUS.CANCEL },
+              {
+                tab: 5,
+                url: `${goPages}&msg=${paymentConfig.PAYMENT_STATUS.CANCEL}`,
+              }
           );
         }
         break;
@@ -914,8 +927,8 @@ const getOrderPay = async (orderNo, message) => {
         // 余额支付:直接提示成功并跳转
         console.log("余额支付");
         return Toast(
-          { title: message },
-          { tab: 5, url: goPages + "&status=1" }
+            { title: message },
+            { tab: 5, url: goPages + "&status=1" }
         );
         break;
       case "weixinApp":
@@ -924,7 +937,7 @@ const getOrderPay = async (orderNo, message) => {
           orderInfo: res.data.prepayWithRequestPaymentResponse,
         });
         let wxPayFailMsg =
-          wxPayResult?.message || paymentConfig.PAYMENT_STATUS.FAIL;
+            wxPayResult?.message || paymentConfig.PAYMENT_STATUS.FAIL;
         if (wxPayResult.status === paymentConfig.PAYMENT_STATUS.SUCCESS) {
           // 查询后端支付状态接口
           let params = {
@@ -933,21 +946,21 @@ const getOrderPay = async (orderNo, message) => {
           };
           let res = await alipayPaymentResult(params);
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.SUCCESS, icon: "success" },
-            { tab: 4, url: goPages }
+              { title: paymentConfig.PAYMENT_STATUS.SUCCESS, icon: "success" },
+              { tab: 4, url: goPages }
           );
         } else if (wxPayResult.status === paymentConfig.PAYMENT_STATUS.FAIL) {
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.FAIL },
-            { tab: 5, url: `${goPages}&msg=${wxPayFailMsg}` }
+              { title: paymentConfig.PAYMENT_STATUS.FAIL },
+              { tab: 5, url: `${goPages}&msg=${wxPayFailMsg}` }
           );
         } else if (wxPayResult.status === paymentConfig.PAYMENT_STATUS.CANCEL) {
           return Toast(
-            { title: paymentConfig.PAYMENT_STATUS.CANCEL },
-            {
-              tab: 5,
-              url: `${goPages}&msg=${paymentConfig.PAYMENT_STATUS.CANCEL}`,
-            }
+              { title: paymentConfig.PAYMENT_STATUS.CANCEL },
+              {
+                tab: 5,
+                url: `${goPages}&msg=${paymentConfig.PAYMENT_STATUS.CANCEL}`,
+              }
           );
         }
         break;
@@ -1028,15 +1041,15 @@ const SubOrder = async (e) => {
 
   // 验证:贝币支付时余额是否充足
   if (
-    orderInfoVo.value.mallType === 1 &&
-    Number(orderInfoVo.value.proTotalFee) > orderInfoVo.value.userIntegral
+      orderInfoVo.value.mallType === 1 &&
+      Number(orderInfoVo.value.proTotalFee) > orderInfoVo.value.userIntegral
   ) {
     return Toast({ title: "贝币余额不足!" });
   }
   // 验证:余额支付时余额是否充足
   else if (
-    data.payType == "yue" &&
-    parseFloat(appStore.$userInfo.nowMoney) <
+      data.payType == "yue" &&
+      parseFloat(appStore.$userInfo.nowMoney) <
       parseFloat(orderInfoVo.value.payFee)
   ) {
     // 余额不足

+ 1 - 1
pages/personal_info/personal_info.vue

@@ -157,7 +157,7 @@
 
 <script setup>
 import { ref, computed, nextTick } from "vue";
-import PayPop from "../../components/pay-pop/pay-pop.vue";
+import PayPop from "../../../components/pay-pop/pay-pop.vue";
 import { useImageUpload } from "@/hooks/useImageUpload";
 import { useAppStore } from "@/stores/app";
 import { onLoad } from "@dcloudio/uni-app";

+ 1 - 1
pages/users/user_goods_collection/index.vue

@@ -14,7 +14,7 @@
           <view v-for="(item,index) in collectProductList" :key="index" class='item acea-row row-middle' style="width: 100%;">
             <up-checkbox :value="item.id.toString()" :checked="item.checked" v-if="!footerswitch"
                       style="margin-right: 10rpx;" activeColor="#ffe079" />
-            <navigator :url='"/pages/goods_details/index?id="+item.productId' hover-class='none'
+            <navigator :url='"/pages/goods/goods_details/index?id="+item.productId' hover-class='none'
                        class="acea-row">
               <view class='pictrue'>
                 <image :src="item.image" mode="aspectFill"></image>

File diff suppressed because it is too large
+ 103 - 0
pages/users/vault/aggrement.vue


File diff suppressed because it is too large
+ 1236 - 0
pages/users/vault/index.vue


+ 603 - 0
pages/users/vault/recycle/order_fill.vue

@@ -0,0 +1,603 @@
+<template>
+  <view class="sell-postend">
+    <view class="sell-postend-banner"></view>
+    <view class="sell-postend-content">
+      <view class="withdraw-express">
+        <view class="header">
+          <view class="header-left">
+            <view class="line"></view>
+            <view class="title">自主邮寄</view>
+          </view>
+          <view class="header-right">
+            <uni-countdown
+              :minute="countdown"
+              color="#f8c007"
+              @timeup="timeupHandle"
+            ></uni-countdown>
+          </view>
+        </view>
+        <view class="item">
+          <view class="targe">收</view>
+          <view class="address">
+            <view
+              >{{ appStore.$wxConfig.mailerName }} :{{
+                appStore.$wxConfig.mailerPhone
+              }}</view
+            >
+            <view class="address-detail" style="margin-top: 4px">
+              <text class="receive-address">地址:</text>
+              {{ appStore.$wxConfig.mailingAddress }}
+            </view>
+          </view>
+          <view class="end">
+            <view class="copy" @click="copy">复制</view>
+          </view>
+        </view>
+      </view>
+      <view class="upload-box">
+        <view class="header">
+          <view class="header-left">
+            <view class="line"></view>
+            <view class="title">上传实物图</view>
+          </view>
+        </view>
+        <view class="upload-tips"
+          >上传实物图、包裹图,如有购买凭证(发票、单据) 可以一起上传</view
+        >
+        <view class="upload-img">
+          <view class="upload-box-contanier">
+            <up-upload
+              :fileList="imageList"
+              uploadIcon="plus"
+              @afterRead="afterRead"
+              @delete="deletePic"
+              name="1"
+              multiple
+              :maxCount="4"
+            >
+              <template #trigger>
+                <view class="upload-block">
+                  <uni-icons
+                    size="38"
+                    color="#ccc"
+                    type="plusempty"
+                  ></uni-icons>
+                </view>
+              </template>
+            </up-upload>
+          </view>
+        </view>
+        <view class="pz-tips">示例图(产品实物,凭证图片)</view>
+        <view class="pz-img">
+          <image
+            v-for="(item, index) in filelist"
+            :key="index"
+            :src="item"
+          ></image>
+        </view>
+      </view>
+      <view class="postend-info-box">
+        <view class="header">
+          <view class="header-left">
+            <view class="line"></view>
+            <view class="title">您的信息</view>
+          </view>
+        </view>
+        <view class="postend-info">
+          <view class="info-item">
+            <view class="info-item-left">快递单号</view>
+            <view class="info-item-right">
+              <input
+                type="text"
+                v-model="expressNum"
+                placeholder="请输入快递单号"
+              />
+              <text class="iconfont icon-iconfontscan" @click="scanCode"></text>
+            </view>
+          </view>
+          <view class="info-agree" @click="showAggre">
+            <up-checkbox-group @change="checkboxChange" v-model="isCheck">
+              <up-checkbox activeColor="#e9c279" label="确认协议"></up-checkbox>
+            </up-checkbox-group>
+
+            <span style="color: #b5aa90; font-size: 16px">《回收协议》</span>
+          </view>
+        </view>
+      </view>
+      <view class="recyle-price">
+        <view class="recyle-desc">预估回收价</view>
+        <view class="recyle-price-num">¥{{ orderInfo.totalMoney }}</view>
+      </view>
+      <view class="btn-box">
+        <!-- <view class="postend-cbtn" @click="cancelOrder">取消订单</view> -->
+        <view class="postend-btn" @click="submitOrder">
+          <image class="btn" src="/static/images/btn-button.png"></image>
+          <text class="btn-text">立即回收</text>
+        </view>
+      </view>
+      <view class="white"></view>
+    </view>
+    <uni-popup
+      ref="singPopup"
+      type="bottom"
+      borderRadius="10px 10px 0 0"
+      maskBackgroundColor="rgba(0,0,0,0)"
+    >
+      <view class="signContent">
+        <scroll-view scrollY class="scroll">
+          <up-parse :content="content"></up-parse>
+        </scroll-view>
+        <view
+          class="comfireBtn footer"
+          @click="
+            isCheck = true;
+            $refs.singPopup.close();
+          "
+        >
+          我已详细知悉
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, onUnmounted } from "vue";
+import { useImageUpload } from "@/hooks/useImageUpload";
+import { recycleCancelAPI, recyclUpdateAPI } from "@/api/functions";
+import { onLoad } from "@dcloudio/uni-app";
+import { agreementGetoneApi } from "@/api/user";
+import { useAppStore } from "@/stores/app";
+const appStore = useAppStore();
+
+// 1. 初始化图片上传钩子
+const { imageList, afterRead, deletePic, uploadLoading } = useImageUpload({
+  pid: 14,
+  model: "recyle",
+});
+console.log(imageList.value);
+
+// 2. 响应式数据
+const pic = ref([]);
+const expressNum = ref("");
+const singPopup = ref(null);
+const isCheck = ref(false);
+const content = ref("");
+const filelist = ref([
+  "https://my-go-easy-im.oss-cn-shenzhen.aliyuncs.com/goeasy-im-%E6%B0%B4%E8%B4%9D%E5%95%86%E5%9F%8E/example/example1.png",
+  "https://my-go-easy-im.oss-cn-shenzhen.aliyuncs.com/goeasy-im-%E6%B0%B4%E8%B4%9D%E5%95%86%E5%9F%8E/example/example2.png",
+  "https://my-go-easy-im.oss-cn-shenzhen.aliyuncs.com/goeasy-im-%E6%B0%B4%E8%B4%9D%E5%95%86%E5%9F%8E/example/example3.png",
+  "https://my-go-easy-im.oss-cn-shenzhen.aliyuncs.com/goeasy-im-%E6%B0%B4%E8%B4%9D%E5%95%86%E5%9F%8E/example/example4.png",
+]);
+const reqData = ref({}); // 假设从路由参数或其他地方获取
+const orderInfo = ref({});
+const countdown = ref(999);
+// 3. 生命周期
+onLoad((options) => {
+  if (options.order) {
+    countdown.value = getTimeDiff(JSON.parse(options.order).expirationTime);
+    orderInfo.value = JSON.parse(options.order);
+  }
+});
+function agreementGetoneFn() {
+  // 资产说明
+  agreementGetoneApi({ name: "recyle" }).then((res) => {
+    content.value = res.data?.content;
+  });
+}
+const checkboxChange = (n) => {
+  isCheck.value = true;
+};
+
+const showAggre = () => {
+  singPopup.value?.open();
+  agreementGetoneFn();
+};
+// 4. 方法
+// 扫描快递单号
+const scanCode = () => {
+  uni.scanCode({
+    success: (res) => {
+      console.log(res);
+      expressNum.value = res.result;
+    },
+    fail: (err) => {
+      uni.showToast({ title: "扫码失败,请重试", icon: "none" });
+    },
+  });
+};
+
+// 复制收件人信息
+const copy = () => {
+  uni.setClipboardData({
+    data: `${appStore.$wxConfig.mailerName}:${appStore.$wxConfig.mailerPhone} 地址: ${appStore.$wxConfig.mailingAddress}`,
+    success() {
+      uni.showToast({ title: "复制成功", icon: "success" });
+    },
+    fail: (err) => {
+      uni.showToast({
+        title: "复制失败",
+        icon: "none",
+      });
+      console.error("复制失败:", err);
+    },
+  });
+};
+
+// 处理图片上传成功
+const success = (result) => {
+  pic.value.push(result.url);
+};
+
+// 提交订单
+const submitOrder = async () => {
+  if (!isCheck.value) {
+    uni.showToast({ title: "请阅读并同意回收协议", icon: "none" });
+    return;
+  }
+
+  if (imageList.value.length === 0) {
+    uni.showToast({ title: "请上传实物图", icon: "none" });
+    return;
+  }
+
+  if (!expressNum.value.trim()) {
+    uni.showToast({ title: "请填写快递单号", icon: "none" });
+    return;
+  }
+  try {
+    uni.showLoading({
+      title: "下单中",
+    });
+    // `实际项目中调用接口`
+    const res = await recyclUpdateAPI({
+      expressNo: expressNum.value,
+      images: imageList.value.map((v) => v.info.url),
+      orderNo: orderInfo.value.orderNo,
+    });
+
+    setTimeout(() => {
+      uni.redirectTo({
+        url: "/pages/users/vault/recycle/recyle_order",
+      });
+    }, 0);
+    uni.hideLoading();
+  } catch (err) {
+    uni.showToast({
+      title: err?.msg || "下单失败,请稍后重试",
+      icon: "none",
+    });
+    setTimeout(() => {
+      uni.redirectTo({
+        url: "/pages/users/vault/recycle/recyle_order",
+      });
+    }, 1000);
+  }
+};
+
+// 取消订单
+const cancelOrder = async () => {
+  try {
+    const res = await recycleCancelAPI({ orderNo: orderInfo.value.orderNo });
+  } catch (err) {
+    console.error("取消订单失败:", err);
+  }
+};
+
+// 订单过期处理
+const timeupHandle = () => {
+  uni.showToast({
+    title: "订单已过期",
+    duration: 1000,
+    icon: "none",
+  });
+
+  // 取消订单
+  cancelOrder();
+
+  setTimeout(() => {
+    uni.reLaunch({
+      url: "/pages/users/vault/recycle/recyle_order",
+    });
+  }, 1000);
+};
+
+// 计算时间差(分钟)
+const getTimeDiff = (targetTimeStr) => {
+  if (!targetTimeStr) return 0;
+
+  const targetTime = new Date(targetTimeStr).getTime();
+  const currentTime = Date.now();
+  const diffMs = targetTime - currentTime;
+  const diffMinutes = diffMs / 60000;
+
+  return diffMinutes > 0 ? diffMinutes : 0;
+};
+</script>
+
+<style scoped lang="scss">
+.signContent {
+  background-color: #f8f8f8;
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  border-radius: 20px 20px 0 0;
+  .scroll {
+    background-color: #fff;
+    padding: 4px;
+    height: 300px;
+    overflow-y: hidden;
+    border: 1px solid #dfdfdf;
+  }
+  .footer {
+    margin-top: 10px;
+    color: #fff;
+    padding: 4px 20px;
+    border-radius: 20px;
+    background: linear-gradient(to right, #8ed187, #5dd665);
+  }
+}
+.icon-iconfontscan {
+  font-size: 20px;
+  color: #f8c007;
+}
+.upload-box-contanier {
+  margin-top: 40rpx;
+  .upload-block {
+    width: 160rpx;
+    height: 160rpx;
+    border: 1px solid #ccc;
+    border-radius: 10rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    color: #ccc;
+    font-weight: 700;
+    font-size: 26rpx;
+  }
+}
+.sell-postend {
+  height: auto;
+  padding: 10rpx 30rpx;
+  position: relative;
+
+  .sell-postend-banner {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 50vh;
+    z-index: -1;
+    // background-image: linear-gradient(360deg, #ffffff 0%, #e8c279 100%);
+    background: $uni-bg-primary;
+  }
+
+  .sell-postend-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .withdraw-express {
+      width: 682rpx;
+      height: 306rpx;
+      background-color: #ffffff;
+      box-shadow: 0rpx 3rpx 13rpx 0rpx rgba(0, 0, 0, 0.13);
+      border-radius: 20rpx;
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+      justify-content: center;
+      box-sizing: border-box;
+      padding: 28rpx;
+      margin-top: 30rpx;
+
+      .item {
+        display: flex;
+        align-items: center;
+        margin-top: 15px;
+
+        .targe {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: #fff;
+          width: 35px;
+          height: 35px;
+          border-radius: 50%;
+          background-color: #f8c007;
+        }
+
+        .address {
+          width: 425rpx;
+          margin: 0 10px;
+          font-size: 28rpx;
+
+          .receive-address {
+            letter-spacing: 9px;
+            line-height: 2;
+          }
+        }
+
+        .end {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+
+          .copy {
+            color: #888888;
+            border-radius: 3px;
+            border: 1px solid #888888;
+            font-size: 24rpx;
+            padding: 0 23rpx;
+          }
+        }
+      }
+    }
+    .upload-box {
+      width: 682rpx;
+      height: auto;
+      background-color: #ffffff;
+      box-shadow: 0rpx 3rpx 13rpx 0rpx rgba(0, 0, 0, 0.13);
+      border-radius: 20rpx;
+      box-sizing: border-box;
+      margin-top: 25rpx;
+      padding: 40rpx 40rpx;
+      .upload-tips {
+        font-size: 26rpx;
+        color: #000000;
+        margin: 30rpx 0;
+      }
+      .upload-img {
+        margin: 30rpx 0;
+      }
+      .pz-tips {
+        text-align: center;
+        font-size: 24rpx;
+        color: #7c7c7c;
+        margin: 40rpx 0;
+      }
+      .pz-img {
+        display: flex;
+        justify-content: space-between;
+        flex-wrap: wrap;
+        margin: 30rpx 0;
+        image {
+          width: 138rpx;
+          height: 138rpx;
+          background-color: #f3f3f3;
+          border-radius: 10rpx;
+          margin: 15rpx 0;
+        }
+      }
+    }
+    .postend-info-box {
+      padding: 40rpx 40rpx;
+      margin-top: 25rpx;
+      width: 682rpx;
+      // height: 527rpx;
+      background-color: #ffffff;
+      box-shadow: 0rpx 3rpx 13rpx 0rpx rgba(0, 0, 0, 0.13);
+      border-radius: 20rpx;
+      box-sizing: border-box;
+      .postend-info {
+        margin-top: 30rpx;
+        .info-item {
+          display: flex;
+          align-items: center;
+          margin: 25rpx 0;
+          .info-item-left {
+            font-size: 26rpx;
+            color: #000000;
+          }
+
+          .info-item-right {
+            margin-left: 15rpx;
+            width: 470rpx;
+            height: 77rpx;
+            background-color: #f3f3f3;
+            border-radius: 10rpx;
+            display: flex;
+            // justify-content: space-around;
+            align-items: center;
+            input {
+              width: 86%;
+              font-size: 26rpx;
+              color: #000;
+              padding-left: 15rpx;
+            }
+          }
+        }
+        .info-agree {
+          display: flex;
+          justify-content: center;
+          margin-top: 25rpx;
+          align-items: center;
+        }
+      }
+    }
+    .recyle-price {
+      display: flex;
+      justify-content: center;
+      font-size: 26rpx;
+      color: #000000;
+      margin: 30rpx 0;
+      .recyle-price-num {
+        // color: #7c7c7c;
+        color: $txt-color;
+        margin-left: 15rpx;
+      }
+    }
+    .btn-box {
+      display: flex;
+      justify-content: center;
+      .postend-btn {
+        // width: 330rpx;
+        width: 267rpx;
+
+        height: 71rpx;
+        position: relative;
+        margin-top: 30rpx;
+        .btn {
+          width: 100%;
+          height: 100%;
+        }
+        .btn-text {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          color: #000;
+          font-size: 26rpx;
+        }
+      }
+      .postend-cbtn {
+        margin-right: 30rpx;
+        // width: 330rpx;
+        padding: 0rpx 60rpx;
+        height: 71rpx;
+        background: #cecece;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: #636363;
+        border-radius: 30rpx;
+        margin-top: 30rpx;
+      }
+    }
+    .white {
+      height: 100rpx;
+    }
+    .header {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .header-left {
+        display: flex;
+        align-items: center;
+        .line {
+          width: 4rpx;
+          height: 28rpx;
+          background-color: #cc9933;
+        }
+        .title {
+          margin-left: 5rpx;
+          font-weight: bold;
+        }
+        .tips {
+          font-size: 26rpx;
+          color: #7c7c7c;
+        }
+      }
+      .header-right {
+        font-size: 26rpx;
+        color: #7c7c7c;
+      }
+    }
+  }
+}
+</style>

+ 416 - 0
pages/users/vault/recycle/recyle_order.vue

@@ -0,0 +1,416 @@
+<template>
+  <view class="list-page">
+    <view class="tabs-box">
+      <up-tabs :list="list" @click="tabsChange" lineColor="#f8c20f"></up-tabs>
+    </view>
+    <view v-if="orderList.length === 0" class="empty">
+      <image
+        style="width: 60%"
+        src="https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/common/empty.png"
+        mode="widthFix"
+      ></image>
+      <text>暂无订单~</text>
+    </view>
+    <view v-else class="inner">
+      <view
+        v-for="(item, index) in orderList"
+        :key="index"
+        class="block"
+        @click="nativeTo(item)"
+      >
+        <view class="header">
+          <view class="title">订单号:{{ item.orderNo }}</view>
+          <view class="tag" :class="['status' + item.status]">
+            {{ getOrderType(item.status) }}
+          </view>
+        </view>
+        <view class="detail">
+          <image
+            @click="previewImage(item.imageList)"
+            style="width: 50px; height: 50px; border-radius: 6px"
+            :src="item.imageList[0] || emptyImg"
+            mode="scaleToFill"
+          ></image>
+          <view class="info">
+            <view>{{ item.metalTypeDesc }}</view>
+            <view class="right">
+              <view>
+                金价:
+                <span class="weight">{{ item.goldPricePerGram }}/g</span>
+              </view>
+              <view>
+                自估重量:
+                <span class="weight">{{ item.estimatedWeight }}克</span>
+              </view>
+            </view>
+          </view>
+        </view>
+        <view class="end">
+          <view class="desc">
+            <view class="">下单时间:{{ item.createTime }}</view>
+            <uni-countdown
+              v-if="item.status == 1"
+              :minute="item.countdown || 0"
+              color="#ff0000"
+              @timeup="timeupHandle(item)"
+            ></uni-countdown>
+          </view>
+        </view>
+        <view v-if="item.status == 1" class="footer"
+          >*请尽快邮寄,倒计时结束后未寄出系统将自动取消订单。</view
+        >
+      </view>
+    </view>
+    <!-- 下拉加载必须代码块 -->
+    <!-- <tm-loadding v-if="loading" style="padding-bottom: 15px"></tm-loadding>
+    <tm-loadding
+      v-if="!loading && !hasMore"
+      success
+      icon="icon-times-circle-fill"
+      label="没有更多啦"
+      color="mymain"
+      style="padding-bottom: 15px"
+    ></tm-loadding> -->
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { recycleListAPI } from "@/api/functions";
+import { useAppStore } from "@/stores/app";
+import {
+  onLoad,
+  onPullDownRefresh,
+  onShow,
+  onReachBottom,
+} from "@dcloudio/uni-app";
+const appStore = useAppStore();
+// 定义数据
+const list = ref([
+  { name: "全部", status: "" },
+  { name: "待寄出", status: 1 },
+  { name: "待检测", status: 3 },
+  { name: "待确认", status: 4 },
+  { name: "已完成", status: 7 },
+]);
+
+const orderList = ref([]);
+const orderAll = ref([]);
+const emptyImg = ref(
+  "https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/recycle/example/example1.png"
+);
+
+// 计算属性
+const pageTop = computed(() => {
+  // return vk.getVuex("$app.totalBarHeight");
+  return 0; // 占位,实际项目中替换为正确的顶部高度计算
+});
+const total = ref(0);
+const hasMore = ref(true);
+const loading = ref(false);
+const params = ref({
+  limit: 10,
+  page: 1,
+  status: "",
+  userId: appStore.userInfo.userId,
+});
+// 初始化
+const init = async (isRefresh = false) => {
+  if (loading.value) return;
+  loading.value = true;
+  uni.showLoading({
+    title: "加载中",
+  });
+  try {
+    // 模拟接口调用
+    const res = await recycleListAPI(params.value);
+    console.log(res);
+
+    const newList = res.data.list?.map((item) => {
+      return {
+        ...item,
+        countdown: getTimeDiff(item.expirationTime),
+        imageList: item.images ? JSON.parse(item.images) : [],
+      };
+    });
+    if (isRefresh) {
+      orderList.value = newList;
+    } else {
+      if (params.value.page === 1) {
+        orderList.value = newList;
+      } else {
+        orderList.value = [...orderList.value, ...newList];
+      }
+    }
+    total.value = res.data.total;
+    hasMore.value = orderList.value.length > total.value;
+    uni.hideLoading();
+  } catch (error) {
+    console.error("获取订单列表失败:", error);
+    uni.showToast({
+      title: "加载失败",
+      icon: "none",
+    });
+  } finally {
+    loading.value = false;
+    // 停止下拉刷新动画
+    if (isRefresh) {
+      uni.stopPullDownRefresh();
+    }
+  }
+};
+
+// 计算目标时间与当前时间的差值(返回分钟数)
+const getTimeDiff = (targetTimeStr) => {
+  const targetTime = new Date(targetTimeStr).getTime();
+  const currentTime = Date.now();
+  const diffMs = targetTime - currentTime;
+  const diffMinutes = diffMs / 60000;
+  return diffMinutes > 0 ? diffMinutes : 0;
+};
+// 时间到结束
+const timeupHandle = (item) => {
+  item.status = 0;
+};
+
+// 标签页切换
+const tabsChange = (item) => {
+  params.value.status = item.status;
+  params.value.page = 1;
+  init();
+};
+
+// 跳转到详情页
+const nativeTo = (item) => {
+  console.log(item);
+  if (item.status == 1) {
+    uni.navigateTo({
+      url: `/pages/users/vault/recycle/order_fill?order=${JSON.stringify(
+        item
+      )}`,
+      // url: "/pages/users/vault/recycle/report",
+    });
+  } else if (item.status == 4 || item.status == 7) {
+    uni.navigateTo({
+      url: `/pages/users/vault/recycle/report?orderInfo=${encodeURIComponent(
+        JSON.stringify(item)
+      )}`,
+    });
+  }
+};
+
+// 计算总重量
+const totalWeight = (order) => {
+  let totalWeight = 0;
+  for (const key in order.cart) {
+    if (Object.hasOwnProperty.call(order.cart, key)) {
+      const item = order.cart[key];
+      totalWeight += Number(item.weight) || 0;
+    }
+  }
+  return totalWeight.toFixed(2);
+};
+
+// 获取商品类别
+const getCate = (cate) => {
+  switch (cate) {
+    case "au":
+      return "黄金";
+    case "ag":
+      return "白银";
+    case "pt":
+      return "铂金";
+    case "kau":
+      return "K金";
+    default:
+      return "其他";
+  }
+};
+
+// 获取订单状态文本
+const getOrderType = (status) => {
+  switch (status) {
+    case 0:
+      return "已取消";
+    case 1:
+      return "待寄出";
+    case 2:
+      return "等待签收";
+    case 3:
+      return "等待检测";
+    case 4:
+      return "确认报告";
+    case 5:
+      return "确认总价";
+    case 6:
+      return "正在打款";
+    case 7:
+      return "交易完成";
+    default:
+      return "未知状态";
+  }
+};
+
+// 生命周期钩子
+onShow(() => {
+  params.value.page = 1;
+  init();
+});
+
+// 下拉刷新
+onPullDownRefresh(() => {});
+
+// 上拉加载更多
+onReachBottom(() => {
+  // 如果没有更多数据或正在加载中,则不执行
+  if (!hasMore.value || loading.value) return;
+
+  // 增加页码
+  params.value.page++;
+  // 加载下一页数据
+  init();
+});
+const previewImage = (urls) => {
+  console.log(urls[0]);
+
+  uni.previewImage({
+    current: urls[0], // 当前显示图片的URL
+    urls: urls, // 需要预览的图片URL列表
+    success: () => {},
+    fail: (err) => {
+      console.error("预览图片失败:", err);
+    },
+  });
+};
+</script>
+
+<style>
+.empty {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff;
+}
+page {
+  height: 100%;
+  background-color: #f8f8f8;
+}
+</style>
+
+<style scoped lang="scss">
+.list-page {
+  background: $uni-bg-primary !important;
+  min-height: 100vh;
+}
+
+.tabs-box {
+  width: 100%;
+  height: 75rpx;
+  // background: #ffffff;
+  box-sizing: border-box;
+  ::v-deep .u-tabs__wrapper__nav__item {
+    padding: 0 37rpx;
+  }
+}
+.tabs {
+  z-index: 100;
+  position: sticky;
+}
+.inner {
+  padding: 10px;
+  .footer {
+    border-radius: 5px;
+    border: 0 0 10px 20px;
+    color: #707070;
+    font-size: 12px;
+    padding: 10px;
+    background-color: rgb(252, 247, 230);
+  }
+}
+.block {
+  margin-bottom: 10px;
+  padding-top: 10px;
+  border-radius: 5px;
+  background-color: #fff;
+  border: 1px solid #cecece;
+  .header {
+    padding: 0 10px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #eee;
+    .tag {
+      font-size: 12px;
+      padding: 2px 5px;
+      border-radius: 4px;
+      color: #ff0800;
+      background-color: #ffcece;
+      &.status-1 {
+        color: #555;
+        background-color: #eeeeee;
+      }
+      &.status1 {
+        color: #ff9900;
+        background-color: #ffeccf;
+      }
+      &.status2 {
+        color: #d6006b;
+        background-color: #fdd3e9;
+      }
+      &.status3 {
+        color: #9900ff;
+        background-color: #e2b7ff;
+      }
+      &.status4,
+      &.status5 {
+        color: rgb(48, 24, 136);
+        background-color: #bbb0fa;
+      }
+      &.status6 {
+        color: #3dac27;
+        background-color: #c8ffb7;
+      }
+    }
+    .title {
+      font-size: 14px;
+    }
+  }
+
+  .detail {
+    display: flex;
+    padding: 10px 13px;
+    .info {
+      flex: 1;
+      width: 100%;
+      font-size: 14px;
+      margin-left: 10px;
+      color: #999;
+
+      .right {
+        min-width: 120px;
+        // display: flex;
+        // justify-content: space-between;
+        .weight {
+          // width: 100%;
+          color: #daa520;
+        }
+      }
+    }
+  }
+  .end {
+    padding: 10px 10px;
+    color: #999;
+    font-size: 14px;
+
+    .desc {
+      margin-bottom: 6px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+  }
+}
+</style>

+ 398 - 0
pages/users/vault/recycle/report.vue

@@ -0,0 +1,398 @@
+<template>
+  <view class="">
+    <view class="banner">
+      <view class="step-box">
+        <up-steps :current="orderStatus" activeColor="#b5aa90">
+          <up-steps-item
+            :title="item.name"
+            v-for="item in stepData"
+            :key="item.name"
+          >
+          </up-steps-item>
+        </up-steps>
+      </view>
+
+      <view class="block">
+        <view class="check">
+          <view class="header">
+            <span class="title">合后检测确认单</span>
+          </view>
+          <view class="desc"
+            >根据双方认可的检测内容及要求进行认定,为客户合后检测认定结果如下:</view
+          >
+          <view class="table">
+            <view class="title">客户寄送品</view>
+            <view class="tab_header">
+              <up-row>
+                <up-col :span="2">
+                  <view class="col">类型</view>
+                </up-col>
+                <up-col :span="2">
+                  <view class="col">来样重</view>
+                </up-col>
+                <up-col :span="2.5">
+                  <view class="col">实际重</view>
+                </up-col>
+
+                <up-col :span="2.5">
+                  <view class="col">成色</view>
+                </up-col>
+                <up-col :span="3">
+                  <view class="col">存入克重</view>
+                </up-col>
+              </up-row>
+            </view>
+            <view class="tab_content">
+              <view>
+                <up-row v-for="item in detectionDetails" :key="item.id">
+                  <up-col :span="2">
+                    <view class="col">{{ getCate(item.type) }}</view>
+                  </up-col>
+                  <up-col :span="2">
+                    <view class="col">{{ item.userEstimatedWeight }}g</view>
+                  </up-col>
+                  <up-col :span="2.5">
+                    <view class="col">{{ item.discountedWeight }}g</view>
+                  </up-col>
+                  <up-col :span="2.5">
+                    <view class="col">{{ item.purity }}%</view>
+                  </up-col>
+                  <up-col :span="3">
+                    <view class="col"
+                      >{{
+                        item.unloadingWeight > 0
+                          ? item.unloadingWeight
+                          : "0.00" || "0.00"
+                      }}g</view
+                    >
+                  </up-col>
+                </up-row>
+              </view>
+            </view>
+          </view>
+          <view class="end">
+            <view class="end-bttom">
+              <span class="total">
+                总计:
+                <span class="price"
+                  >{{ detectionReport.totalDiscountedWeight }}g</span
+                >
+                回收价:
+                <span class="price">¥{{ detectionReport.totalPrice }}</span>
+              </span>
+            </view>
+            <view
+              class="end-bttom"
+              style="margin-top: 10rpx"
+              v-if="detectionReport.totalDeductedDeposit"
+            >
+              <span class="total">
+                扣除保证金:
+                <span class="price"
+                  >¥{{ detectionReport.totalDeductedDeposit }}</span
+                >
+              </span>
+            </view>
+            <view class="end-bttom" style="margin-top: 10rpx">
+              <span class="total">
+                快递费用:
+                <span class="price">¥{{ deliveryCosts }}</span>
+              </span>
+            </view>
+            <view class="extra"
+              >即:客户对{{
+                detectionReport.inspectionTime
+              }}合后检测重量和回收价无异议</view
+            >
+            <view class="sign">
+              <span>检测人:{{ detectionReport.nikeName || "系统" }}</span>
+            </view>
+            <view class="time">
+              {{ detectionReport.inspectionTime }}
+            </view>
+          </view>
+          <view style="font-size: 14px; padding: 10px">
+            <span style="font-size: 16px; font-weight: 700">注:</span>
+            <span>折算公式:兑料重量 = 折足重量 = 来料熔后重量 * 折足成色</span>
+          </view>
+        </view>
+      </view>
+      <view class="block">
+        <view class="check">
+          <view class="header">
+            <span class="title">检测图片</span>
+          </view>
+          <view class="checlImage">
+            <cl-upload
+              v-model="detectionImages"
+              :add="false"
+              :remove="false"
+            ></cl-upload>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view style="height: 70px"></view>
+    <view class="footer" v-if="orderStatus == 4">
+      <view class="btn" @click="next()">确认报告</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { onMounted, ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import { recyclDetectionReportAPI } from "@/api/functions";
+const stepData = ref([
+  {
+    name: "待填写",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待邮寄",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待签收",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待检测",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待确认",
+    isNow: 1,
+    type: 0,
+  },
+
+  {
+    name: "待打款",
+    isNow: 0,
+    type: 0,
+  },
+  {
+    name: "完成",
+    isNow: 0,
+    type: 0,
+  },
+]);
+// 注意:onLoad是uni-app的生命周期,如果是纯Vue3,应使用onMounted
+
+// 替代Vue2的methods中的方法
+const getCate = (cate) => {
+  console.log(cate);
+
+  switch (cate) {
+    case 1:
+      return "黄金";
+    case 3:
+      return "白银";
+    case 2:
+      return "铂金";
+    case 4:
+      return "K金";
+  }
+};
+// 检测信息-成色类型等
+const detectionDetails = ref([]);
+// 检测详情
+const detectionReport = ref({});
+// 检测图片
+const detectionImages = ref([]);
+// 订单号
+const orderNo = ref("");
+// 订单状态
+const orderStatus = ref(4);
+// 快递费用
+const deliveryCosts = ref(0);
+onLoad((options) => {
+  if (options.orderInfo) {
+    // 先解码再解析
+    const orderInfoStr = decodeURIComponent(options.orderInfo);
+    const res = JSON.parse(orderInfoStr);
+    detectionDetails.value = res.detectionDetails;
+
+    detectionReport.value = res.detectionReport;
+    detectionImages.value = JSON.parse(res.detectionReport.detectionImages);
+    orderNo.value = res.id;
+    orderStatus.value = res.status;
+    deliveryCosts.value = res.deliveryCosts;
+  }
+});
+// 确认报告
+const next = async () => {
+  uni.showLoading({
+    title: "确认中",
+  });
+  const res = await recyclDetectionReportAPI(orderNo.value);
+  uni.hideLoading();
+  setTimeout(() => {
+    uni.redirectTo({
+      url: "/pages/users/vault/recycle/recyle_order",
+    });
+  }, 1000);
+};
+</script>
+
+<style scoped lang="scss">
+.banner {
+  padding: 10px 15px;
+  background: $uni-bg-primary; /* fallback for old browsers */
+
+  .step-box {
+    margin: 30rpx 0;
+    width: 100%;
+    background: #fff;
+    height: 150rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    border-radius: 4px;
+
+    &::v-deep .u-text__value {
+      font-size: 24rpx !important;
+    }
+  }
+}
+
+.block {
+  margin-bottom: 10px;
+
+  .check {
+    background-color: #fff;
+    border-radius: 4px;
+    padding-top: 10px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+  .header {
+    font-size: 14px;
+    font-weight: 700;
+    padding-left: 10px;
+    margin-left: 10px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    &::before {
+      position: absolute; /*绝对定位*/
+      top: 50%; /*Y轴方向偏移自身高度的50%*/
+      transform: translatey(-40%); /*Y轴方向偏移微调*/
+      left: 0px; /*紧靠容器左边缘*/
+      content: ""; /*伪元素需要有内容才能显示*/
+      width: 2px; /*伪元素宽度*/
+      height: 18px; /*伪元素高度*/
+      background-color: #daa520; /*伪元素颜色*/
+    }
+    .title {
+      font-size: 18px;
+    }
+  }
+  .desc {
+    padding: 8px 10px;
+    color: #888;
+    font-size: 14px;
+  }
+}
+.table {
+  padding: 14px 10px;
+  .title {
+    text-align: center;
+    padding: 4px 0;
+    color: #fff;
+    font-weight: 700;
+    font-size: 20rpx;
+    width: 100%;
+    background-color: #b5aa90;
+  }
+  .tab_header {
+    border-left: 1px solid #b5aa90;
+    font-size: 18px;
+    background-color: #fff;
+    .col {
+      padding: 12px 0;
+      line-height: 19px;
+      font-size: 22rpx;
+    }
+  }
+  .tab_content {
+    border-left: 1px solid #b5aa90;
+    font-size: 16px;
+    background-color: #fff;
+    .col {
+      padding: 12px 0;
+      line-height: 19px;
+      font-size: 22rpx;
+    }
+  }
+  .tab_header .col,
+  .tab_content .col {
+    border-bottom: 1px solid #b5aa90;
+    border-right: 1px solid #b5aa90;
+    text-align: center;
+    font-size: 22rpx;
+  }
+  .symbol-name {
+    font-family: Microsoft YaHei, Arial, Helvetica, sans-serif;
+    font-size: 20px;
+    color: #ffbf24;
+  }
+}
+.end {
+  width: 100%;
+  display: inline-block;
+  text-align: right;
+  .end-bttom {
+    color: #888;
+    .total {
+      font-size: 16px;
+    }
+    .price {
+      margin-right: 10px;
+      color: #ffa034;
+    }
+  }
+  .extra {
+    font-size: 14px;
+    margin-top: 15px;
+    color: #888;
+    margin-right: 10px;
+  }
+  .sign {
+    margin: 10px;
+  }
+  .time {
+    padding: 10px;
+  }
+}
+
+.checlImage {
+  padding: 10px;
+}
+.footer {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
+  position: fixed;
+  width: 100%;
+  border-radius: 8px 8px 0 0;
+  background-color: #fff;
+  bottom: 0;
+  height: 60px;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  .btn {
+    color: #fff;
+    font-weight: 700;
+    margin: 0 15px;
+    border-radius: 10px;
+    width: 100px;
+    padding: 10px;
+    text-align: center;
+    background-color: #cc9933;
+  }
+}
+</style>

+ 701 - 0
pages/users/vault/storeMetal/GoldMailForm.vue

@@ -0,0 +1,701 @@
+<template>
+  <view class="withdraw">
+    <view class="content">
+      <view class="withdraw-express">
+        <view class="header">
+          <span class="title">自主邮寄</span>
+        </view>
+        <view class="item">
+          <view class="targe">收</view>
+          <view class="address">
+            <view
+              >{{ appStore.$wxConfig.mailerName }} :{{
+                appStore.$wxConfig.mailerPhone
+              }}</view
+            >
+            <view class="address-detail" style="margin-top: 4px">
+              <text class="receive-address">地址:</text>
+              {{ appStore.$wxConfig.mailingAddress }}
+            </view>
+          </view>
+          <view class="end">
+            <view class="copy" @click="copyAddress">复制</view>
+          </view>
+        </view>
+      </view>
+      <view>
+        <view style="padding: 0rpx 10px">
+          <view class="gold-item">
+            <view class="header">
+              <span class="title">快递单号</span>
+            </view>
+            <view class="input-box">
+              <input
+                type="text"
+                class="inpu-box-ds"
+                placeholder="请输入快递单号"
+                v-model="expressNum"
+                placeholder-style="color: #999999; font-size: 28rpx;"
+              />
+              <text class="iconfont icon-iconfontscan" @click="scanCode"></text>
+            </view>
+          </view>
+
+          <view class="gold-item">
+            <view class="header">
+              <span class="title">快递图片</span>
+            </view>
+            <view style="margin-top: 10px">
+              <view class="upload-box">
+                <up-upload
+                  :fileList="imageList"
+                  uploadIcon="plus"
+                  @afterRead="afterRead"
+                  @delete="deletePic"
+                  name="1"
+                  multiple
+                  :maxCount="3"
+                >
+                  <template #trigger>
+                    <view class="upload-block">
+                      <uni-icons
+                        size="38"
+                        color="#ccc"
+                        type="plusempty"
+                      ></uni-icons>
+                    </view>
+                  </template>
+                </up-upload>
+              </view>
+            </view>
+          </view>
+
+          <view class="gold-item">
+            <view class="form-body">
+              <view class="weight-input">
+                <view class="input-label">黄金金重</view>
+                <view class="form-body-box">
+                  <input
+                    v-model="auExtract"
+                    type="digit"
+                    class="weight-value"
+                  />
+                  <text>g</text>
+                </view>
+              </view>
+              <view class="weight-input">
+                <view class="input-label">K金金重</view>
+                <view class="form-body-box">
+                  <input
+                    v-model="kauExtract"
+                    type="digit"
+                    class="weight-value"
+                  />
+                  <text>g</text>
+                </view>
+              </view>
+              <view class="weight-input">
+                <view class="input-label">铂金金重</view>
+                <view class="form-body-box">
+                  <input
+                    v-model="ptExtract"
+                    type="digit"
+                    class="weight-value"
+                  />
+                  <text>g</text>
+                </view>
+              </view>
+              <view class="weight-input">
+                <view class="input-label">白银金重</view>
+                <view class="form-body-box">
+                  <input
+                    v-model="agExtract"
+                    type="digit"
+                    class="weight-value"
+                  />
+                  <text>g</text>
+                </view>
+              </view>
+
+              <view class="tips">* 若无相关产品,可不用填写</view>
+            </view>
+          </view>
+        </view>
+        <view class="withdraw-body">
+          <view class="tx-active" style="margin-top: 10px">
+            <button @click="submitGoldOrder">点击提交</button>
+          </view>
+          <view class="aggregate" @click="aggregate = !aggregate">
+            <image
+              class="choose"
+              :src="
+                aggregate
+                  ? '/static/recycle/choose.png'
+                  : '/static/recycle/nochoose.png'
+              "
+              mode="scaleToFill"
+            ></image>
+            <view class="aggre">
+              阅读并同意
+              <span class="aggre-text" @click="showAggre">《存金协议》</span>
+            </view>
+          </view>
+          <view style="margin-top: 20px">
+            <up-parse :content="content"></up-parse>
+          </view>
+        </view>
+      </view>
+    </view>
+    <uni-popup
+      background-color="#fff"
+      ref="singPopup"
+      type="bottom"
+      :safeArea="false"
+      borderRadius="10px 10px 0 0"
+      maskBackgroundColor="rgba(0, 0, 0,0)"
+    >
+      <view class="signContent">
+        <scroll-view scrollY class="scroll">
+          <up-parse :content="agreement"></up-parse>
+        </scroll-view>
+        <view
+          class="comfireBtn footer"
+          @click="
+            aggregate = true;
+            $refs.singPopup.close();
+          "
+        >
+          我已详细知悉
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+// 1. 导入依赖(保持不变)
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import { depositCreateAPI } from "@/api/functions";
+import { useImageUpload } from "@/hooks/useImageUpload";
+import { useAppStore } from "@/stores/app";
+const appStore = useAppStore();
+
+// 2. 响应式变量(保持不变,明确金属类型对应关系)
+const { imageList, afterRead, deletePic, uploadLoading } = useImageUpload({
+  pid: 9,
+  model: "gold",
+});
+const extract = ref([]);
+const auExtract = ref(null); // 黄金重量 → type:1
+const ptExtract = ref(null); // 铂金重量 → type:2
+const agExtract = ref(null); // 白银重量 → type:3
+const kauExtract = ref(null); // K金重量 → type:4
+const aggregate = ref(false);
+const expressNum = ref("");
+const cust_img = ref([]);
+const singPopup = ref(null);
+const props = defineProps(["agreement", "content"]);
+onLoad(() => {});
+const showAggre = () => {
+  singPopup.value?.open();
+};
+// 3. 扫码方法(保持不变)
+const scanCode = () => {
+  uni.scanCode({
+    success: (res) => {
+      expressNum.value = res.result;
+      console.log("赋值后:", expressNum.value);
+    },
+    fail: (err) => {
+      console.error("扫码失败:", err);
+      uni.showToast({ title: "扫码失败,请重试", icon: "none" });
+    },
+  });
+};
+const copy = (item) => {
+  uni.setClipboardData({
+    data: item,
+    success: () => {
+      uni.showToast({
+        title: "复制成功",
+        icon: "success",
+      });
+    },
+    fail: (err) => {
+      uni.showToast({
+        title: "复制失败",
+        icon: "none",
+      });
+      console.error("复制失败:", err);
+    },
+  });
+};
+
+// 复制地址方法
+const copyAddress = () => {
+  uni.setClipboardData({
+    data: `${appStore.$wxConfig.mailerName}:${appStore.$wxConfig.mailerPhone} 地址: ${appStore.$wxConfig.mailingAddress}`,
+    success() {
+      uni.showToast({ title: "复制成功", icon: "success" });
+    },
+    fail: (err) => {
+      uni.showToast({
+        title: "复制失败",
+        icon: "none",
+      });
+      console.error("复制失败:", err);
+    },
+  });
+};
+
+/**
+ * 转换金属重量为目标格式:[{type:1,weight:1}, ...]
+ * 改用直接引用ref变量的映射表,避免eval
+ */
+const buildMetalWeightList = () => {
+  // 优化:金属映射表直接包含ref变量,无需通过字符串动态获取
+  const metalConfigList = [
+    { metalVar: auExtract, type: 1, label: "黄金" }, // 直接传auExtract(ref变量)
+    { metalVar: ptExtract, type: 2, label: "铂金" },
+    { metalVar: agExtract, type: 3, label: "白银" },
+    { metalVar: kauExtract, type: 4, label: "K金" },
+  ];
+
+  // 循环收集有效重量(逻辑与之前一致,仅获取重量的方式变化)
+  const metalList = [];
+  metalConfigList.forEach(({ metalVar, type }) => {
+    // 直接访问ref变量的.value获取重量值
+    const weightValue = metalVar.value;
+    // 处理重量:去空格、排除空值、转为数字
+    const trimmedWeight = weightValue ? String(weightValue).trim() : "";
+    const validWeight = Number(trimmedWeight);
+
+    // 仅保留「非空、非NaN、正数」的重量
+    if (trimmedWeight !== "" && !isNaN(validWeight) && validWeight > 0) {
+      metalList.push({
+        type: type,
+        weight: validWeight, // 转为数字格式,符合接口要求
+      });
+    }
+  });
+
+  return metalList;
+};
+
+// 5. 提交方法(逻辑不变,依赖优化后的buildMetalWeightList)
+const submitGoldOrder = async () => {
+  // 步骤1:基础校验(图片+重量+快递单号)
+  const isCheckPass = checkHandle();
+  if (!isCheckPass) return;
+
+  // 步骤2:构建目标格式的金属重量数组
+  const metalWeightList = buildMetalWeightList();
+  if (metalWeightList.length === 0) {
+    uni.showToast({ title: "请至少填写一个有效金属重量!", icon: "none" });
+    return;
+  }
+
+  // 步骤3:整合提交数据
+  const submitData = {
+    metalInfos: metalWeightList, // 核心目标数组
+    expressNo: expressNum.value.trim(),
+    expressImages: imageList.value.map((v) => v.info.url),
+  };
+
+  // 步骤4:执行提交(示例接口调用)
+  try {
+    const res = await depositCreateAPI(submitData);
+
+    uni.showToast({ title: "提交成功!" });
+    setTimeout(() => {
+      uni.navigateTo({
+        url: "/pages/users/vault/storeMetal/order",
+      });
+    }, 1000);
+  } catch (error) {
+    console.error("提交失败:", error);
+    uni.showToast({ title: "提交失败,请重试", icon: "none" });
+  }
+};
+
+// 6. 校验方法(逻辑不变,依赖优化后的buildMetalWeightList)
+const checkHandle = () => {
+  // 校验1:快递图片
+  if (imageList.value.length === 0) {
+    uni.showToast({ title: "请上传快递图片!", icon: "none" });
+    return false;
+  }
+
+  // 校验2:至少一个有效金属重量
+  const metalWeightList = buildMetalWeightList();
+  if (metalWeightList.length === 0) {
+    uni.showToast({
+      title: "请至少填写一个金属重量(黄金/铂金/白银/K金)!",
+      icon: "none",
+    });
+    return false;
+  }
+
+  // 校验3:快递单号(可选,根据需求保留)
+  if (!expressNum.value.trim()) {
+    uni.showToast({ title: "请填写或扫码获取快递单号!", icon: "none" });
+    return false;
+  }
+
+  return true;
+};
+</script>
+
+<style lang="scss" scoped>
+page {
+  height: 100%;
+}
+.icon-iconfontscan {
+  font-size: 20px;
+}
+.upload-box {
+  .upload-block {
+    width: 160rpx;
+    height: 160rpx;
+    border: 1px solid #ccc;
+    border-radius: 10rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    color: #ccc;
+    font-weight: 700;
+    font-size: 26rpx;
+  }
+}
+.tips {
+  text-align: center;
+  color: #bdbdbd;
+  font-size: 24rpx;
+  margin: 20rpx auto;
+}
+
+.form-body {
+  // background-color: #fff;
+  // padding: 20px 30px;
+  font-size: 14px;
+  margin-top: 90rpx;
+
+  .weight-input {
+    display: flex;
+    align-items: center;
+    // justify-content: space-around;
+    margin: 30rpx 0;
+
+    .form-body-box {
+      display: flex;
+      align-items: center;
+      background-color: #ededed;
+      margin-left: 30rpx;
+      height: 72rpx;
+      border-radius: 10rpx;
+      font-size: 30rpx;
+
+      text {
+        color: #cc9933;
+        padding-right: 20rpx;
+        font-size: 25rpx;
+      }
+
+      input {
+        // font-size: 18rpx;
+        padding-left: 10rpx;
+      }
+    }
+
+    .input-label {
+      font-size: 32rpx;
+      font-weight: 500;
+
+      width: 135rpx;
+    }
+
+    .weight-value {
+      height: 1.9em;
+      font-size: 1.5em;
+      border: none;
+      outline: none;
+    }
+  }
+
+  .submit-btn-area {
+    button {
+      color: #fff;
+      background: #c4bba6;
+    }
+  }
+
+  .agreement-section {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 20rpx;
+    margin-top: 30rpx;
+
+    .agreement-checkbox {
+      width: 32rpx;
+      height: 16px;
+    }
+
+    .agreement-text {
+      font-size: 14px;
+      margin-left: 10px;
+
+      .agreement-link {
+        color: #3ab0ff;
+      }
+    }
+  }
+}
+
+.input-box {
+  display: flex;
+  background-color: #ededed;
+  border-radius: 5px;
+  height: 90rpx;
+  align-items: center;
+  justify-content: space-around;
+  font-size: 28rpx;
+  // paddingft: ;
+  padding-left: 20rpx;
+
+  // color:#c7c7c7 ;
+  input {
+    // background-color: ;
+    width: 80%;
+    // color: #c7c7c7;
+  }
+}
+
+.inpu-box-zl {
+  justify-content: space-between;
+  padding-left: 47rpx;
+}
+
+.gold-item {
+  margin: 50rpx 0;
+}
+
+// .gold-input{
+//    background-color: red !important;
+// }
+.address-detail {
+  line-height: 2.2;
+}
+
+.header {
+  padding-left: 5px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 10px;
+  border-radius: 5px;
+  // background-color: #fff;
+  font-weight: bold;
+  font-size: 18px;
+
+  &::before {
+    position: absolute;
+    /*绝对定位*/
+    top: 50%;
+    /*Y轴方向偏移自身高度的50%*/
+    transform: translatey(-50%);
+    /*Y轴方向偏移微调*/
+    left: 0;
+    /*紧靠容器左边缘*/
+    content: "";
+    /*伪元素需要有内容才能显示*/
+    width: 2px;
+    /*伪元素宽度*/
+    height: 15px;
+    /*伪元素高度*/
+    background-color: #daa520;
+    /*伪元素颜色*/
+  }
+
+  .title {
+    font-weight: 500;
+    font-size: 32rpx;
+    font-family: "黑体";
+  }
+}
+
+.withdraw {
+  height: 100%;
+  background-color: #f7f7f7;
+  border-radius: 10px 10px 0 0;
+  position: relative;
+  // top: -20rpx;
+  padding: 0 25rpx;
+  top: -67rpx;
+
+  .withdraw-express {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    justify-content: center;
+
+    padding: 28rpx;
+    margin-bottom: 30rpx;
+    border-radius: 30rpx;
+    padding-right: 0;
+
+    .head {
+      font-size: 14px;
+    }
+
+    .item {
+      display: flex;
+      align-items: center;
+      margin-top: 15px;
+
+      .targe {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: #fff;
+        width: 35px;
+        height: 35px;
+        border-radius: 50%;
+        background-color: #cc9933;
+      }
+
+      .address {
+        width: 430rpx;
+        margin: 0 10px;
+        font-size: 28rpx;
+
+        .receive-address {
+          letter-spacing: 9px;
+        }
+      }
+
+      .end {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .copy {
+          color: #888888;
+          border-radius: 3px;
+          border: 1px solid #888888;
+          font-size: 24rpx;
+          padding: 6rpx 23rpx;
+        }
+      }
+    }
+  }
+
+  .withdraw-body {
+    // background-color: #fff;
+    padding: 20px 30px;
+    font-size: 14px;
+
+    .input-money {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-weight: 600;
+      border-bottom: 1px solid #eaeef1;
+
+      .rmb {
+        font-size: 16px;
+      }
+
+      .t-input {
+        height: 1.9em;
+        font-size: 1.5em;
+        border: none;
+        outline: none;
+      }
+    }
+
+    .aggregate {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 10px;
+      color: #ababab;
+      font-size: 12px;
+
+      .aggre {
+        font-size: 30rpx;
+        margin-left: 10px;
+
+        .aggre-text {
+          color: #cc9933;
+        }
+      }
+    }
+
+    .choose {
+      width: 28rpx;
+      height: 28rpx;
+    }
+  }
+}
+
+.tx {
+  button {
+    color: #b2b2b2;
+  }
+}
+
+.tx-active {
+  margin: 0 auto;
+  display: flex;
+  justify-content: center;
+
+  button {
+    width: 420rpx;
+    height: 84rpx;
+    background-color: #cc9933;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 16px;
+    border-radius: 30px;
+  }
+}
+
+.signContent {
+  // background-color: #f8f8f8;
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  border-radius: 20px 20px 0 0;
+
+  .scroll {
+    // background-color: #fff;
+    padding: 4px;
+    height: 300px;
+    overflow-y: hidden;
+    border: 1px solid #dfdfdf;
+  }
+
+  .footer {
+    margin-top: 10px;
+    color: #fff;
+    padding: 4px 20px;
+    border-radius: 20px;
+    background: linear-gradient(to right, #8ed187, #5dd665);
+  }
+}
+
+.express-input {
+  &:nth-child(1) {
+    padding: 0;
+  }
+}
+</style>

+ 363 - 0
pages/users/vault/storeMetal/gmReport.vue

@@ -0,0 +1,363 @@
+<template>
+  <view class="">
+    <view class="banner">
+      <view class="step-box">
+        <up-steps :current="orderStatus" activeColor="#b5aa90">
+          <up-steps-item
+            :title="item.name"
+            v-for="item in stepData"
+            :key="item.name"
+          >
+          </up-steps-item>
+        </up-steps>
+      </view>
+
+      <view class="block">
+        <view class="check">
+          <view class="header">
+            <span class="title">合后检测确认单</span>
+          </view>
+          <view class="desc"
+            >根据双方认可的检测内容及要求进行认定,为客户合后检测认定结果如下:</view
+          >
+          <view class="table">
+            <view class="title">客户寄送品</view>
+            <view class="tab_header">
+              <up-row>
+                <up-col :span="3">
+                  <view class="col">类型</view>
+                </up-col>
+                <up-col :span="3">
+                  <view class="col">来样重</view>
+                </up-col>
+                <up-col :span="3">
+                  <view class="col">实际重</view>
+                </up-col>
+                <up-col :span="3">
+                  <view class="col">成色</view>
+                </up-col>
+              </up-row>
+            </view>
+            <view class="tab_content">
+              <view>
+                <up-row v-for="item in detectionDetails" :key="item.id">
+                  <up-col :span="3">
+                    <view class="col">{{ getCate(item.type) }}</view>
+                  </up-col>
+                  <up-col :span="3">
+                    <view class="col"
+                      >{{ Number(item.userEstimatedWeight).toFixed(2) }}g</view
+                    >
+                  </up-col>
+                  <up-col :span="3">
+                    <view class="col"
+                      >{{ Number(item.discountedWeight).toFixed(2) }}g</view
+                    >
+                  </up-col>
+                  <up-col :span="3">
+                    <view class="col">{{ item.purity }}%</view>
+                  </up-col>
+                </up-row>
+              </view>
+            </view>
+          </view>
+          <view class="end">
+            <view class="end-bttom">
+              <span class="total">
+                总计:
+                <span class="price">{{ deTotalWeight.toFixed(2) }}g</span>
+              </span>
+            </view>
+            <view class="extra"
+              >即:客户对{{
+                detectionReport.inspectionTime
+              }}合后检测重量和回收价无异议</view
+            >
+            <view class="sign">
+              <span
+                >检测人:{{ detectionReport.inspectorNickname || "系统" }}</span
+              >
+            </view>
+            <view class="time">
+              {{ detectionReport.inspectionTime }}
+            </view>
+          </view>
+          <view style="font-size: 14px; padding: 10px">
+            <span style="font-size: 16px; font-weight: 700">注:</span>
+            <span>折算公式:兑料重量 = 折足重量 = 来料熔后重量 * 折足成色</span>
+          </view>
+        </view>
+      </view>
+      <view class="block">
+        <view class="check">
+          <view class="header">
+            <span class="title">检测图片</span>
+          </view>
+          <view class="checlImage">
+            <cl-upload
+              v-model="detectionImages"
+              :add="false"
+              :remove="false"
+            ></cl-upload>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view style="height: 70px"></view>
+    <view class="footer" v-if="orderStatus == 2">
+      <view class="btn" @click="next()">确认报告</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { onMounted, computed, ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import { postalDepositAPI } from "@/api/functions";
+const stepData = ref([
+  {
+    name: "待收货",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待检测",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待确认",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "待充值",
+    isNow: 0,
+    type: 1,
+  },
+  {
+    name: "已完成",
+    isNow: 1,
+    type: 0,
+  },
+]);
+// 注意:onLoad是uni-app的生命周期,如果是纯Vue3,应使用onMounted
+
+// 替代Vue2的methods中的方法
+const getCate = (cate) => {
+  // console.log(cate);
+
+  switch (cate) {
+    case 1:
+      return "黄金";
+    case 3:
+      return "白银";
+    case 2:
+      return "铂金";
+    case 4:
+      return "K金";
+  }
+};
+// 检测信息-成色类型等
+const detectionDetails = ref([]);
+// 检测详情
+const detectionReport = ref({});
+// 检测图片
+const detectionImages = ref([]);
+// 订单号
+const orderNo = ref("");
+// 订单状态
+const orderStatus = ref(4);
+onLoad((options) => {
+  if (options.orderInfo) {
+    // 先解码再解析
+    const orderInfoStr = decodeURIComponent(options.orderInfo);
+    const res = JSON.parse(orderInfoStr);
+    detectionDetails.value = res.goldBalances;
+
+    detectionReport.value = res;
+    detectionImages.value = JSON.parse(res.inspectionImage);
+    orderNo.value = res.id;
+    orderStatus.value = res.status;
+  }
+});
+// 确认报告
+const next = async () => {
+  const res = await postalDepositAPI(orderNo.value);
+  uni.showToast({
+    title: "确认成功",
+    duration: 1000,
+  });
+  setTimeout(() => {
+    uni.redirectTo({
+      url: "/pages/users/vault/storeMetal/order",
+    });
+  }, 1000);
+};
+const deTotalWeight = computed(() => {
+  if (detectionReport.value.goldBalances) {
+    const totalWeight = detectionReport.value.goldBalances.reduce(
+      (sum, material) => {
+        return sum + (Number(material.discountedWeight) || 0);
+      },
+      0
+    );
+    return totalWeight;
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.banner {
+  padding: 10px 15px;
+  background: $uni-bg-primary; /* fallback for old browsers */
+
+  .step-box {
+    margin: 30rpx 0;
+    width: 100%;
+    background: #fff;
+    height: 150rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    border-radius: 4px;
+
+    &::v-deep .u-text__value {
+      font-size: 24rpx !important;
+    }
+  }
+}
+
+.block {
+  margin-bottom: 10px;
+
+  .check {
+    background-color: #fff;
+    border-radius: 4px;
+    padding-top: 10px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+  .header {
+    font-size: 14px;
+    font-weight: 700;
+    padding-left: 10px;
+    margin-left: 10px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    &::before {
+      position: absolute; /*绝对定位*/
+      top: 50%; /*Y轴方向偏移自身高度的50%*/
+      transform: translatey(-40%); /*Y轴方向偏移微调*/
+      left: 0px; /*紧靠容器左边缘*/
+      content: ""; /*伪元素需要有内容才能显示*/
+      width: 2px; /*伪元素宽度*/
+      height: 18px; /*伪元素高度*/
+      background-color: #daa520; /*伪元素颜色*/
+    }
+    .title {
+      font-size: 18px;
+    }
+  }
+  .desc {
+    padding: 8px 10px;
+    color: #888;
+    font-size: 14px;
+  }
+}
+.table {
+  padding: 14px 10px;
+  .title {
+    text-align: center;
+    padding: 4px 0;
+    color: #fff;
+    font-weight: 700;
+    font-size: 16px;
+    width: 100%;
+    background-color: #b5aa90;
+  }
+  .tab_header {
+    border-left: 1px solid #b5aa90;
+    font-size: 18px;
+    background-color: #fff;
+    .col {
+      padding: 12px 0;
+      line-height: 19px;
+    }
+  }
+  .tab_content {
+    border-left: 1px solid #b5aa90;
+    font-size: 16px;
+    background-color: #fff;
+    .col {
+      padding: 12px 0;
+      line-height: 19px;
+    }
+  }
+  .tab_header .col,
+  .tab_content .col {
+    border-bottom: 1px solid #b5aa90;
+    border-right: 1px solid #b5aa90;
+    text-align: center;
+  }
+  .symbol-name {
+    font-family: Microsoft YaHei, Arial, Helvetica, sans-serif;
+    font-size: 20px;
+    color: #ffbf24;
+  }
+}
+.end {
+  width: 100%;
+  display: inline-block;
+  text-align: right;
+  .end-bttom {
+    color: #888;
+    .total {
+      font-size: 16px;
+    }
+    .price {
+      margin-right: 10px;
+      color: #ffa034;
+    }
+  }
+  .extra {
+    font-size: 14px;
+    margin-top: 15px;
+    color: #888;
+    margin-right: 10px;
+  }
+  .sign {
+    margin: 10px;
+  }
+  .time {
+    padding: 10px;
+  }
+}
+
+.checlImage {
+  padding: 10px;
+}
+.footer {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
+  position: fixed;
+  width: 100%;
+  border-radius: 8px 8px 0 0;
+  background-color: #fff;
+  bottom: 0;
+  height: 60px;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  .btn {
+    color: #fff;
+    font-weight: 700;
+    margin: 0 15px;
+    border-radius: 10px;
+    width: 100px;
+    padding: 10px;
+    text-align: center;
+    background-color: #cc9933;
+  }
+}
+</style>

+ 161 - 0
pages/users/vault/storeMetal/goldBullionStock.vue

@@ -0,0 +1,161 @@
+<template>
+  <view class="content">
+    <view class="tabs-wrap">
+      <!-- 新增tabs导航 -->
+      <view class="takeout-tabs">
+        <view
+          class="tab-item"
+          :class="['tab-active', currentTab === 0 ? 'active' : '']"
+          @click="currentTab = 0"
+          >邮寄存金</view
+        >
+        <view
+          class="tab-item"
+          :key="currentTab"
+          :class="['tab-active', currentTab === 1 ? 'active' : '']"
+          @click="currentTab = 1"
+          >无物流存金</view
+        >
+      </view>
+    </view>
+    <view v-if="!currentTab">
+      <gold-mail-form :content="content" :agreement="agreement">
+      </gold-mail-form>
+    </view>
+    <view v-else>
+      <non-logistics-gold :viprealGoldprice="viprealGoldprice">
+      </non-logistics-gold>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from "vue";
+import GoldMailForm from "./GoldMailForm.vue";
+import nonLogisticsGold from "./nonLogisticsGold.vue";
+import { agreementGetoneApi } from "@/api/user";
+import { useStoreRights } from "@/stores/rights";
+import useRealGoldPrice from "@/hooks/useRealGoldPrice";
+import { number } from "../../../../uni_modules/uview-plus/libs/function/test";
+const {
+  realGoldprice, // 黄金实时销售价(基础)
+} = useRealGoldPrice({});
+const rightsStore = useStoreRights();
+// 响应式数据
+const currentTab = ref(0);
+const content = ref("");
+const agreement = ref("");
+// Vue 3 中使用 onMounted 替代 onLoad
+// 获取协议
+function agreementGetoneFn() {
+  agreementGetoneApi({ name: "saveGoldRules" }).then((res) => {
+    content.value = res.data?.content;
+  });
+  // 资产说明
+  agreementGetoneApi({ name: "saveGold" }).then((res) => {
+    agreement.value = res.data?.content;
+  });
+}
+
+// 黄金调整价
+const adjustGoldprice = computed(() => {
+  const res = rightsStore.userBenefits.nobleMeta?.find(
+    (gold) => gold.name == "黄金"
+  ) || 0;
+  return res;
+});
+// 获取实时金价
+const viprealGoldprice = computed(
+  () =>
+    Number(realGoldprice.value) -
+    Number(rightsStore.userBenefits.buy) +
+    Number(adjustGoldprice.value?.sellPriceAdjust)
+);
+onMounted((options) => {
+  agreementGetoneFn();
+});
+
+// const someComputed = computed(() => { ... })
+
+// function someMethod() { ... }
+</script>
+
+<style scoped lang="scss">
+/* 新增tabs样式 */
+.takeout-tabs {
+  display: flex;
+  height: 120rpx;
+  overflow: hidden;
+  position: relative;
+  top: 85rpx;
+}
+
+.tabs-wrap {
+  height: 270rpx;
+  width: 100%;
+}
+
+.tab-item {
+  width: 50%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  box-sizing: border-box;
+  color: #fff;
+}
+
+.tab-item.active {
+  color: #fff;
+  font-weight: bold;
+}
+
+.tab-item.active::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  /* 修改为0,使三角形显示在底部 */
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 100;
+  border-style: solid;
+  border-width: 0 20rpx 20rpx 20rpx;
+  /* 使用rpx单位 */
+  border-color: transparent transparent #f7f7f7 transparent;
+  width: 0;
+  height: 0;
+  opacity: 1;
+  /* 设置为可见 */
+  transform-origin: center top;
+  animation: showTriangle 0.3s ease-out forwards;
+}
+
+/* 添加动画定义 */
+@keyframes showTriangle {
+  from {
+    opacity: 0;
+    transform: translate(-50%, 10rpx);
+  }
+
+  to {
+    opacity: 1;
+    transform: translate(-50%, 0);
+  }
+}
+
+.content {
+  position: relative;
+  height: 100%;
+  // background-color: #ededed;
+  // background: url('https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/static/20250529221733.jpg') top center no-repeat;
+  // background-size: 100% 40%;
+  // background-color: #f7f7f7;
+  background: $uni-bg-primary !important;
+}
+
+::v-deep .uni-input-placeholder {
+  font-size: 14px;
+  color: #bdbdbd;
+}
+</style>

File diff suppressed because it is too large
+ 1103 - 0
pages/users/vault/storeMetal/index.vue


+ 817 - 0
pages/users/vault/storeMetal/metalExchange.vue

@@ -0,0 +1,817 @@
+<template>
+  <view class="withdraw">
+    <view class="withdrawContent">
+      <view class="tabs">
+        <view
+          v-for="item in tabsList"
+          :key="item.key"
+          class="tabs-item"
+          :class="[tabsIndex === item.key ? 'active' : '']"
+          @click="tabsChange(item)"
+        >
+          {{ item.title }}
+        </view>
+      </view>
+      <!-- 收货地址模块 -->
+      <view class="address" @click="onAddress">
+        <view class="addressCon" v-if="addressInfo.realName">
+          <view class="name"
+            >{{ addressInfo.realName }}
+            <text class="phone">{{ addressInfo.phone }}</text>
+          </view>
+          <view class="acea-row">
+            <text class="default font-color" v-if="addressInfo.isDefault"
+              >[默认]</text
+            >
+            <text class="line2"
+              >{{ addressInfo.province }}{{ addressInfo.city
+              }}{{ addressInfo.district }}{{ addressInfo.detail }}</text
+            >
+          </view>
+        </view>
+        <view class="addressCon" v-else>
+          <view class="setaddress">设置收货地址</view>
+        </view>
+        <view class="iconfont icon-jiantou"></view>
+      </view>
+      <view class="content-top">
+        <!-- 快递公司选择模块 -->
+        <view class="section">
+          <view class="section-title">选择快递公司</view>
+          <view class="courier-list">
+            <view
+              class="courier-item"
+              :class="{ active: item.isSelected }"
+              v-for="(item, index) in courierList"
+              :key="index"
+              @click="handleSelectCourier(index)"
+            >
+              <image class="courier-logo" :src="item.logo"></image>
+              <image
+                v-show="item.isSelected"
+                class="gou"
+                src="https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/courier/dui.png"
+              ></image>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="gold-box">
+        <view class="gold-item">
+          <view class="header">
+            <h3 class="title">板料克重</h3>
+            <view class="live-gold">
+              实时金价
+              <text class="price">{{ realprice.toFixed(2) }}</text>
+            </view>
+          </view>
+          <view class="input-box">
+            <input
+              v-model.number="extract"
+              placeholder="请输入克数"
+              type="digit"
+              class="t-input"
+              @input="onKeyInput"
+            />
+          </view>
+          <view class="infoMoney" style="font-size: 16px">
+            <view v-if="is_out">
+              <text class="info-money-num" style="color: #ff1e0f">
+                输入克重超过可提现克重,账户克重{{ accountWeight }}克
+              </text>
+            </view>
+            <view v-else-if="is_lowest">
+              <text class="info-money-num" style="color: #ff1e0f">
+                最低{{ lowest }}克起兑换,账户克重{{
+                  accountWeight
+                }}克,且最多两位小数
+              </text>
+              <text class="info-money-num" style="color: #ff1e0f"
+                >最低{{ lowest }}g起购买,且最多两位小数</text
+              >
+            </view>
+            <text v-else class="infoMoneyNum">
+              {{ `手续费:${1}元 运费:${1}元 账户克重:${accountWeight}克` }}
+            </text>
+            <view class="infoTip">*默认融成小圆饼寄出</view>
+          </view>
+        </view>
+      </view>
+
+      <view class="withdraw-bottom">
+        <view
+          :class="['submitBtn', is_post ? '' : 'submitBtnActive']"
+          style="margin-top: 10px"
+        >
+          <button @click="handleShowModel">提交兑换</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- <uni-popup
+      ref="singPopup"
+      type="bottom"
+      borderRadius="10px 10px 0 0"
+      maskBackgroundColor="rgba(0,0,0,0)"
+    >
+      <view class="signContent">
+        <scroll-view scrollY class="scrollView">
+          <rich-text :nodes="agreement"></rich-text>
+        </scroll-view>
+        <view
+          class="confirmBtn footer"
+          @click="
+            aggregate = true;
+            $refs.singPopup.close();
+          "
+        >
+          我已详细知悉
+        </view>
+      </view>
+    </uni-popup> -->
+    <!-- 收货地址组件 -->
+    <addressWindow
+      ref="addressWindowRef"
+      :address="address"
+      :pagesUrl="pagesUrl"
+      @OnDefaultAddress="OnDefaultAddress"
+      @OnChangeAddress="OnChangeAddress"
+      @changeClose="changeClose"
+    />
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, nextTick } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+// 导入用户地址详情API接口
+import { getAddressDetail, getAddressDefault } from "@/api/user.js";
+// 导入地址选择组件
+import addressWindow from "@/components/addressWindow";
+import { useAppStore } from "@/stores/app";
+const appStore = useAppStore();
+console.log(appStore.userInfo);
+
+const tabsList = ref([
+  { key: 1, label: "gold", title: "黄金" },
+  { key: 2, label: "platinum", title: "铂金" },
+  { key: 3, label: "silver", title: "白银" },
+]);
+// 响应式数据(对应原data())
+const tabsIndex = ref(1);
+const needPrice = ref(0); // 手续费
+const goldAllPrice = ref(0); // 总金价
+const extract = ref(0);
+const lowest = ref(1);
+const is_out = ref(false);
+const is_post = ref(false);
+const is_lowest = ref(false);
+const aggregate = ref(false);
+const agreement = ref([]);
+
+// 快递公司列表(静态结构,内部选中状态需响应式)
+const courierList = ref([
+  {
+    type: 2,
+    name: "顺丰陆运",
+    logo: "https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/courier/sf-land.png",
+    isSelected: true,
+    price: 15,
+  },
+  {
+    type: 3,
+    name: "顺丰空运",
+    logo: "https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/courier/sf-air.png",
+    isSelected: false,
+    price: 24,
+  },
+  {
+    type: 4,
+    name: "顺丰到付",
+    logo: "https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/courier/sf-cod.png",
+    isSelected: false,
+    price: 0,
+  },
+]);
+
+// 快递相关响应式数据
+const courierPrice = ref(15);
+const selectedCourierType = ref(2);
+const selectedGender = ref("gold");
+
+// 用户余额及手续费(响应式对象)
+const preciousMetal = ref({
+  gold: { name: "黄金", pool: 0, fee: 0 },
+  platinum: { name: "铂金", pool: 0, fee: 0 },
+  silver: { name: "白银", pool: 0, fee: 0 },
+});
+const tabsChange = (item) => {
+  tabsIndex.value = item.key;
+  selectedGender.value = item.label;
+};
+const handleSelectCourier = (index) => {
+  courierList.value.forEach((item, i) => {
+    item.isSelected = i === index;
+  });
+
+  const selectedCourier = courierList.value.find((item) => item.isSelected);
+  if (selectedCourier) {
+    // this.courierPrice = this.userInfo?.isSVIP
+    //   ? vk.myfn.accDivDecimal(selectedCourier.price, 2)
+    //   : selectedCourier.price;
+    // this.selectedCourierType = selectedCourier.type; // 保存选中快递的type
+  }
+};
+
+// 输入框逻辑
+// 在<script setup>中添加输入验证方法
+const onKeyInput = () => {
+  // 重置提示状态
+  is_lowest.value = false;
+  is_out.value = false;
+
+  // 处理空输入
+  if (extract.value === null || extract.value === "" || isNaN(extract.value)) {
+    is_lowest.value = true; // 空输入时显示最低限制提示
+    return;
+  }
+
+  const inputWeight = Number(extract.value);
+  const minWeight = lowest.value; // 最低1克(来自定义的lowest: ref(1))
+  const accountWeightNum = Number(accountWeight.value) || 0;
+
+  // 验证1:是否低于最低限制(1克)
+  const isBelowMin = inputWeight < minWeight;
+
+  // 验证2:是否超过两位小数(通过正则判断)
+  const hasMoreThanTwoDecimals = /\.\d{3,}$/.test(extract.value.toString());
+
+  // 验证3:是否超过账户可用克重
+  const isOverAccount = inputWeight > accountWeightNum;
+
+  // 设置提示状态
+  if (isBelowMin || hasMoreThanTwoDecimals) {
+    is_lowest.value = true; // 显示最低限制提示(包含兑换和购买)
+  }
+  if (isOverAccount) {
+    is_out.value = true; // 显示超过账户克重提示
+  }
+};
+
+// 账户克重
+const accountWeight = computed(() => {
+  if (tabsIndex.value == 1) {
+    return appStore.userInfo.goldBalance;
+  }
+  if (tabsIndex.value == 2) {
+    return appStore.userInfo.ptBalance;
+  }
+  if (tabsIndex.value == 3) {
+    return appStore.userInfo.agBalance;
+  }
+});
+import useRealGoldPrice from "@/hooks/useRealGoldPrice";
+// 实时价格处理
+const {
+  realGoldRecyclePrice,
+  realKGoldRecyclePrice,
+  realPtRecyclePrice,
+  realAgRecyclePrice,
+} = useRealGoldPrice({});
+import { useStoreRights } from "@/stores/rights";
+const rightsStore = useStoreRights();
+// 回收
+const vipRealGoldRecyclePrice = computed(() => {
+  let res = 0;
+  const basePrice = Number(realGoldRecyclePrice.value || 0);
+  const soldBenefit = Number(rightsStore.userBenefits?.sold || 0);
+  res = appStore.userInfo.svip
+    ? basePrice + soldBenefit + 0.3
+    : basePrice + soldBenefit;
+
+  return res;
+});
+const viprealKGoldRecyclePrice = computed(() => {
+  let res = 0;
+  const basePrice = Number(realKGoldRecyclePrice.value || 0);
+  const soldBenefit = Number(rightsStore.userBenefits?.sold || 0);
+  res = appStore.userInfo.svip
+    ? basePrice + soldBenefit + 0.3
+    : basePrice + soldBenefit;
+
+  return res;
+});
+const viprealPtRecyclePrice = computed(() => {
+  let res = 0;
+  const basePrice = Number(realPtRecyclePrice.value || 0);
+  const soldBenefit = Number(rightsStore.userBenefits?.sold || 0);
+  res = appStore.userInfo.svip
+    ? basePrice + soldBenefit + 0.3
+    : basePrice + soldBenefit;
+
+  return res;
+});
+const viprealAgRecyclePrice = computed(() => {
+  let res = 0;
+  const basePrice = Number(realAgRecyclePrice.value || 0);
+  const soldBenefit = Number(rightsStore.userBenefits?.sold || 0);
+  res = appStore.userInfo.svip
+    ? basePrice + soldBenefit + 0.3
+    : basePrice + soldBenefit;
+
+  return res;
+});
+// 实时金价
+const realprice = computed(() => {
+  if (tabsIndex.value == 1) {
+    return vipRealGoldRecyclePrice.value;
+  }
+
+  if (tabsIndex.value == 2) {
+    return viprealPtRecyclePrice.value;
+  }
+  if (tabsIndex.value == 3) {
+    return viprealAgRecyclePrice.value;
+  }
+});
+// 地址相关
+const textareaStatus = ref(true);
+const pagesUrl = ref("");
+const addressWindowRef = ref(null);
+const address = ref({
+  address: false,
+  addressId: 0,
+}); // 地址组件
+const addressInfo = ref({}); // 地址信息
+const addressId = ref(0); // 地址id
+// 首次进入页面时展示默认地址
+const OnDefaultAddress = (e) => {
+  console.log("保存默认地址详情");
+
+  addressInfo.value = e; // 保存默认地址详情
+  address.value.addressId = e.id; // 更新选中地址ID
+};
+// 打开地址选择弹窗
+const onAddress = () => {
+  textareaStatus.value = false; // 隐藏备注框
+  address.value.address = true; // 显示地址弹窗
+
+  // 设置地址页面跳转链接
+  pagesUrl.value = "/pages/users/user_address_list/index";
+  nextTick(() => {
+    addressWindowRef.value.fetchAddressList();
+  });
+};
+// 选择地址后触发的事件
+const OnChangeAddress = (e) => {
+  console.log("OnChangeAddress", e);
+
+  addressInfo.value = e; // 保存选中的地址详情
+  address.value.addressId = e.id; // 更新选中地址ID
+  textareaStatus.value = true; // 显示备注框
+  address.value.address = false; // 关闭地址弹窗
+};
+// 获取默认地址或指定地址详情
+const getaddressInfo = () => {
+  if (addressId.value) {
+    // 若有指定地址ID,获取该地址详情
+    getAddressDetail(addressId.value).then((res) => {
+      if (res.data) {
+        // 若地址存在
+        res.data.isDefault = parseInt(res.data.isDefault); // 转换默认地址标识为数字
+        addressInfo.value = res.data || {}; // 保存地址详情
+        addressId.value = res.data.id || 0; // 更新地址ID
+        address.value.addressId = res.data.id || 0; // 更新选中地址ID
+      }
+    });
+  } else {
+    // 若没有指定地址ID,获取默认地址
+    getAddressDefault().then((res) => {
+      // 注意:原代码中未导入getAddressDefault,可能是遗漏
+      if (res.data) {
+        // 若默认地址存在
+        res.data.isDefault = parseInt(res.data.isDefault);
+        addressInfo.value = res.data || {};
+        addressId.value = res.data.id || 0;
+        address.value.addressId = res.data.id || 0;
+      }
+    });
+  }
+};
+onLoad(() => {
+  // 若已登录且非支付页面,则获取地址信息(注释掉的逻辑,预留)
+  if (appStore.isLogin) {
+    // console.log(1111);
+
+    getaddressInfo();
+    // 等待DOM更新后,调用地址组件的方法获取地址列表
+    nextTick(() => {
+      addressWindowRef.value.fetchAddressList();
+    });
+  }
+});
+// 关闭地址弹窗
+const changeClose = () => {
+  address.value.address = false; // 隐藏地址弹窗
+};
+</script>
+
+<style lang="scss" scoped>
+$item-value-color: #dca537;
+$card-bcolor: #f8f8f8;
+
+page {
+  height: 100%;
+  background-color: #ededed;
+}
+.tabs {
+  display: flex;
+  height: 80rpx;
+  border-radius: 40rpx;
+  padding: 0 20rpx;
+  color: #fff;
+  font-size: 38rpx;
+  font-weight: 300;
+  // margin-bottom: 10rpx;
+
+  .tabs-item {
+    width: 50%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    // align-items:;
+    position: relative;
+    color: #000000;
+  }
+
+  .active::after {
+    position: absolute;
+    bottom: 18rpx;
+    left: 50%;
+    transform: translateX(-50%);
+    content: "";
+    z-index: 100;
+    display: block;
+    width: 65rpx;
+    height: 6rpx;
+    background-color: #cc9933;
+  }
+}
+.withdraw-bottom {
+  width: 100%;
+  margin-top: 100rpx;
+  display: flex;
+  justify-content: center;
+}
+
+.header {
+  padding-left: 5px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 10px;
+  border-radius: 5px;
+  // background-color: #fff;
+  // font-weight: bold;
+  font-size: 18px;
+
+  .live-gold {
+    font-weight: 0;
+    font-size: 30rpx;
+
+    .price {
+      color: #dcbe81;
+      margin-left: 10rpx;
+    }
+  }
+
+  .item {
+    display: flex;
+    align-items: center;
+    margin-top: 15px;
+
+    .targe {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+      width: 35px;
+      height: 35px;
+      border-radius: 50%;
+      background-color: #cc9933;
+    }
+
+    .address {
+      width: 440rpx;
+      margin: 0 10px;
+      font-size: 28rpx;
+
+      .receive-address {
+        letter-spacing: 9px;
+      }
+    }
+
+    .end {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .copy {
+        color: #888888;
+        border-radius: 3px;
+        border: 1px solid #888888;
+        font-size: 24rpx;
+        padding: 6rpx 23rpx;
+      }
+    }
+  }
+
+  &::before {
+    position: absolute;
+    top: 50%;
+    transform: translatey(-50%);
+    left: 0;
+    content: "";
+    width: 2px;
+    height: 15px;
+    background-color: #daa520;
+  }
+
+  .title {
+    font-weight: 500;
+    font-size: 32rpx;
+    font-family: "黑体";
+  }
+}
+
+.gold-box {
+  padding: 30rpx 20rpx;
+}
+
+.gold-item {
+  margin: 0 0 25rpx 0;
+}
+
+.input-box {
+  display: flex;
+  background-color: #ededed;
+  border-radius: 5px;
+  height: 90rpx;
+  align-items: center;
+  justify-content: space-around;
+  font-size: 28rpx;
+  padding-left: 20rpx;
+  margin-top: 50rpx;
+
+  input {
+    width: 90%;
+  }
+}
+// 地址相关样式
+.address {
+  width: 690rpx;
+  max-height: 180rpx;
+  margin: 40rpx 0;
+  padding: 28rpx;
+  box-sizing: border-box;
+  border-radius: 30rpx;
+
+  .addressCon {
+    width: 596rpx;
+    font-size: 26rpx;
+    color: #666;
+
+    .name {
+      font-size: 30rpx;
+      color: #282828;
+      font-weight: bold;
+      margin-bottom: 10rpx;
+
+      .phone {
+        margin-left: 50rpx;
+      }
+    }
+
+    .default {
+      margin-right: 12rpx;
+    }
+
+    .setaddress {
+      color: #333;
+      font-size: 28rpx;
+    }
+  }
+
+  .iconfont {
+    font-size: 35rpx;
+    color: #707070;
+  }
+}
+.content-top {
+  border-radius: 30rpx;
+  // padding: 20rpx;
+  background: $card-bcolor;
+  margin-bottom: 20rpx;
+
+  // 通用区块标题
+  .section {
+    // margin: 0 24rpx 32rpx;
+
+    .section-title {
+      font-size: 32rpx;
+      font-weight: bold;
+      margin-bottom: 24rpx;
+      position: relative;
+      padding-left: 20rpx;
+
+      &::after {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 3px;
+        background: $item-value-color;
+        content: "";
+      }
+    }
+
+    // 快递公司选择
+    .courier-list {
+      display: flex;
+      // overflow-x: auto;
+      justify-content: space-between;
+      padding-bottom: 16rpx;
+
+      .courier-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        min-width: 160rpx;
+        margin-right: 24rpx;
+        padding: 24rpx 20rpx;
+        border: 2rpx solid #eee;
+        border-radius: 12rpx;
+        cursor: pointer;
+        position: relative;
+
+        .gou {
+          position: absolute;
+          width: 40rpx;
+          height: 40rpx;
+          // background: #dca537;
+          right: -7rpx;
+          bottom: -7rpx;
+          border-radius: 50%;
+          right: -6rpx;
+          bottom: -6rpx;
+        }
+
+        &.active {
+          border-color: #dbb870;
+        }
+
+        .courier-logo {
+          width: 120rpx;
+          height: 120rpx;
+          margin-bottom: 8rpx;
+        }
+
+        .courier-name {
+          font-size: 26rpx;
+        }
+      }
+    }
+  }
+}
+
+.withdraw {
+  height: 100%;
+  background-size: 100% 40%;
+  background: $uni-bg-primary !important;
+  .withdrawContent {
+    padding: 45rpx 40rpx;
+    background-color: #f7f7f7;
+    // margin-top: ;
+    position: relative;
+    top: 150rpx;
+    border-radius: 50rpx 50rpx 0 0;
+  }
+
+  .withdrawBody {
+    background-color: #fff;
+    padding: 20px 30px;
+    font-size: 14px;
+
+    .inputMoney {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-weight: 600;
+      border-bottom: 1px solid #eaeef1;
+
+      .rmb {
+        font-size: 16px;
+        // text-wrap: nowrap;
+      }
+
+      .tInput {
+        height: 1.9em;
+        font-size: 2.5em;
+        border: none;
+        position: relative;
+        left: 3.5%;
+        outline: none;
+      }
+    }
+  }
+}
+
+.infoMoney {
+  margin-top: 10px;
+  font-size: 12px;
+  margin-bottom: 20px;
+
+  .infoMoneyNum {
+    color: #b2b2b2;
+    font-size: 26rpx;
+  }
+
+  .infoTip {
+    color: red;
+    font-size: 23rpx;
+    margin-top: 10rpx;
+  }
+}
+
+.agreement {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px;
+
+  .chooseIcon {
+    width: 16px;
+    height: 16px;
+  }
+
+  .agreementText {
+    font-size: 14px;
+    margin-left: 10px;
+    color: #999999;
+
+    .agreementLink {
+      color: #dca12b;
+    }
+  }
+}
+
+.submitBtn {
+  button {
+    background-color: #dca12b;
+    color: #fff;
+    width: 450rpx;
+    height: 72rpx;
+    display: flex;
+    font-size: 30rpx;
+    justify-content: center;
+    align-items: center;
+    border-radius: 30rpx;
+  }
+}
+
+.submitBtnActive {
+  button {
+    color: #fff;
+    background: #c4bba6;
+  }
+}
+
+.signContent {
+  background-color: #f8f8f8;
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  border-radius: 20px 20px 0 0;
+
+  .scrollView {
+    background-color: #fff;
+    padding: 4px;
+    height: 300px;
+    overflow-y: hidden;
+    border: 1px solid #dfdfdf;
+  }
+
+  .confirmBtn {
+    margin-top: 10px;
+    color: #fff;
+    padding: 4px 20px;
+    border-radius: 20px;
+    background: linear-gradient(to right, #8ed187, #5dd665);
+  }
+}
+</style>

+ 508 - 0
pages/users/vault/storeMetal/nonLogisticsGold.vue

@@ -0,0 +1,508 @@
+<template>
+  <view class="withdraw">
+    <view class="content">
+      <view class="withdraw-body">
+        <view class="gold-box">
+          <view class="gold-item">
+            <view class="header">
+              <h3 class="title">存金克重</h3>
+              <view class="live-gold">
+                实时金价
+                <text class="price">{{ viprealGoldprice.toFixed(2) }}</text>
+              </view>
+            </view>
+
+            <view class="input-box">
+              <input
+                type="text"
+                class="inpu-box-ds"
+                placeholder="请输入克重"
+                v-model="extract"
+                @input="onKeyInput"
+                placeholder-style="color: #999999; font-size: 28rpx;"
+              />
+            </view>
+            <!-- <view class="header">
+              <span class="title">淘宝订单号</span>
+            </view>
+            <view class="input-box">
+              <input
+                type="text"
+                class="inpu-box-ds"
+                placeholder="请输入淘宝定单号"
+                v-model="expressNo"
+                @input="onKeyInput"
+                placeholder-style="color: #999999; font-size: 28rpx;"
+              />
+            </view> -->
+          </view>
+        </view>
+        <view class="info-money" style="font-size: 16px">
+          <text class="info-money-num" v-if="extract"
+            >预存金额= {{ extract }} * {{ viprealGoldprice.toFixed(2) }} =
+            {{ totalPrice }}元</text
+          >
+          <text class="info-money-num" v-else>预存金额0元</text>
+        </view>
+        <view class="img-container">
+          <view class="img-title">
+            <span class="title">实物图片</span>
+          </view>
+          <view class="img-box" style="margin-top: 10px">
+            <view class="upload-box">
+              <up-upload
+                :fileList="imageList"
+                uploadIcon="plus"
+                @afterRead="afterRead"
+                @delete="deletePic"
+                name="1"
+                multiple
+                :maxCount="3"
+              >
+                <template #trigger>
+                  <view class="upload-block">
+                    <uni-icons
+                      size="38"
+                      color="#ccc"
+                      type="plusempty"
+                    ></uni-icons>
+                  </view>
+                </template>
+              </up-upload>
+            </view>
+          </view>
+        </view>
+        <view class="submit-box">
+          <view
+            :class="'tx' + (is_post ? '' : '-active')"
+            style="margin-top: 10px"
+            class="submit"
+          >
+            <button @click="handleShowModel">提交存金</button>
+          </view>
+          <view class="aggregate" @click="aggregate = !aggregate">
+            <image
+              class="choose"
+              :src="
+                aggregate
+                  ? '/static/recycle/choose.png'
+                  : '/static/recycle/nochoose.png'
+              "
+              mode="scaleToFill"
+            ></image>
+            <view class="aggre">
+              阅读并同意
+              <span class="aggre-text" @click="showAggre">《存金协议》</span>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+    <uni-popup
+      ref="singPopup"
+      type="bottom"
+      borderRadius="10px 10px 0 0"
+      maskBackgroundColor="rgba(0,0,0,0)"
+    >
+      <view class="signContent">
+        <scroll-view scrollY class="scroll">
+          <up-parse :content="agreement"></up-parse>
+        </scroll-view>
+        <view
+          class="comfireBtn footer"
+          @click="
+            aggregate = true;
+            $refs.singPopup.close();
+          "
+        >
+          我已详细知悉
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, watch } from "vue";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+
+import { noLogisticsCreateAPI } from "@/api/functions";
+import { useImageUpload } from "@/hooks/useImageUpload";
+import { agreementGetoneApi } from "@/api/user";
+import useRealGoldPrice from "@/hooks/useRealGoldPrice";
+const { imageList, afterRead, deletePic, uploadLoading } = useImageUpload({
+  pid: 9,
+  model: "gold",
+});
+const maoding = ref(0);
+const props = defineProps(["viprealGoldprice"]);
+const singPopup = ref(null);
+const agreement = ref("");
+const type = ref("store"); // 初始值与原data一致
+const is_lock = ref(false);
+const needPrice = ref(0);
+const extract = ref(null);
+const is_post = ref(false);
+const totalPrice = ref(0);
+const aggregate = ref(false);
+// const expressNo = ref("");
+// 获取实时金价
+const { realGoldprice } = useRealGoldPrice("RTJ_Au");
+
+const viprealGoldprice = computed(() => Number(realGoldprice.value));
+
+// 获取协议
+function agreementGetoneFn() {
+  // 资产说明
+  agreementGetoneApi({ name: "saveGold" }).then((res) => {
+    agreement.value = res.data?.content;
+  });
+}
+// 页面生命周期(对应原onLoad)
+onShow(() => {});
+const showAggre = () => {
+  agreementGetoneFn();
+  singPopup.value?.open();
+};
+
+const onKeyInput = (e) => {
+  extract.value = e.target.value;
+};
+
+// 提交存金
+const handleShowModel = async () => {
+  if (!extract.value) {
+    return uni.showToast({
+      title: "请输入克重",
+      duration: 2000,
+      icon: "none",
+    });
+  }
+  // if (!expressNo.value) {
+  //   return uni.showToast({
+  //     title: "请输入快递单号",
+  //     duration: 2000,
+  //     icon: "none",
+  //   });
+  // }
+  const res = await noLogisticsCreateAPI({
+    depositWeight: extract.value,
+    image: imageList.value.map((v) => v.info.url),
+    // taobaoOrderNo: expressNo.value,
+  });
+  uni.showToast({ title: "下单成功!" });
+
+  setTimeout(() => {
+    uni.navigateTo({
+      url: "/pages/users/vault/index",
+    });
+  }, 1000);
+};
+watch(
+  () => extract.value,
+  (val) => {
+    if (val) {
+      totalPrice.value = (
+        Number(extract.value) * Number(props.viprealGoldprice)
+      ).toFixed(2);
+      is_post.value = true;
+    } else {
+      is_post.value = false;
+    }
+  },
+  { immediate: true }
+);
+</script>
+
+<style lang="scss" scoped>
+page {
+  height: 100%;
+  background-color: #ededed;
+}
+.upload-box {
+  .upload-block {
+    width: 160rpx;
+    height: 160rpx;
+    border: 1px solid #ccc;
+    border-radius: 10rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    color: #ccc;
+    font-weight: 700;
+    font-size: 26rpx;
+  }
+}
+.info-money-num {
+  color: #999999;
+  font-size: 26rpx;
+}
+
+.submit-box {
+  margin-top: 300rpx;
+}
+
+.submit {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  button {
+    background-color: #dca12b;
+    color: #fff;
+    width: 450rpx;
+    height: 72rpx;
+    display: flex;
+    font-size: 30rpx;
+    justify-content: center;
+    align-items: center;
+    border-radius: 30rpx;
+  }
+}
+
+.header {
+  padding-left: 5px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 10px;
+  border-radius: 5px;
+  // background-color: #fff;
+  // font-weight: bold;
+  font-size: 18px;
+
+  .live-gold {
+    font-weight: 500;
+    font-size: 32rpx;
+    font-family: "黑体";
+
+    .price {
+      color: #d0a34a;
+      margin-left: 10rpx;
+      font-weight: bold;
+      font-size: 38rpx;
+    }
+  }
+
+  .item {
+    display: flex;
+    align-items: center;
+    margin-top: 15px;
+
+    .targe {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+      width: 35px;
+      height: 35px;
+      border-radius: 50%;
+      background-color: #cc9933;
+    }
+
+    .address {
+      width: 440rpx;
+      margin: 0 10px;
+      font-size: 28rpx;
+
+      .receive-address {
+        letter-spacing: 9px;
+      }
+    }
+
+    .end {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .copy {
+        color: #888888;
+        border-radius: 3px;
+        border: 1px solid #888888;
+        font-size: 24rpx;
+        padding: 6rpx 23rpx;
+      }
+    }
+  }
+
+  &::before {
+    position: absolute;
+    /*绝对定位*/
+    top: 50%;
+    /*Y轴方向偏移自身高度的50%*/
+    transform: translatey(-50%);
+    /*Y轴方向偏移微调*/
+    left: 0;
+    /*紧靠容器左边缘*/
+    content: "";
+    /*伪元素需要有内容才能显示*/
+    width: 2px;
+    /*伪元素宽度*/
+    height: 15px;
+    /*伪元素高度*/
+    background-color: #daa520;
+    /*伪元素颜色*/
+  }
+
+  .title {
+    font-weight: 500;
+    font-size: 32rpx;
+    font-family: "黑体";
+  }
+}
+
+.gold-box {
+  padding-top: 30rpx;
+  .gold-item {
+    .input-box {
+      display: flex;
+      background-color: #ededed;
+      border-radius: 5px;
+      height: 90rpx;
+      align-items: center;
+      justify-content: space-around;
+      font-size: 28rpx;
+
+      margin: 20rpx 0;
+
+      // color:#c7c7c7 ;
+      input {
+        padding-left: 12rpx;
+        width: 92%;
+      }
+    }
+  }
+}
+
+.img-container {
+  padding: 30rpx 0;
+
+  .img-title {
+    padding-left: 5px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 10px;
+    border-radius: 5px;
+    font-weight: 500;
+    font-size: 32rpx;
+    font-family: "黑体";
+
+    &::before {
+      position: absolute;
+      top: 50%;
+      transform: translatey(-50%);
+      left: 0;
+      content: "";
+      width: 2px;
+      height: 15px;
+      background-color: #daa520;
+    }
+  }
+}
+
+.withdraw {
+  height: 100%;
+  background-color: #f7f7f7;
+  height: 100%;
+
+  border-radius: 10px 10px 0 0;
+  position: relative;
+  // top: -20rpx;
+  padding: 0 25rpx;
+  top: -67rpx;
+
+  &-body {
+    // background-color: #fff;
+    padding: 0 28rpx;
+    font-size: 14px;
+
+    .input-money {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-weight: 600;
+      border-bottom: 1px solid #eaeef1;
+
+      .rmb {
+        font-size: 16px;
+      }
+
+      .t-input {
+        height: 1.9em;
+        font-size: 2.5em;
+        border: none;
+        position: relative;
+        left: 3.5%;
+        outline: none;
+      }
+    }
+
+    .info-money {
+      font-size: 26rpx;
+      margin-left: 34rpx;
+      &-num {
+        color: #c9c9c9;
+      }
+    }
+
+    .aggregate {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 10px;
+
+      .aggre {
+        font-size: 28rpx;
+        margin-left: 10px;
+        color: #929292;
+
+        .aggre-text {
+          color: #cc9933;
+        }
+      }
+    }
+
+    .choose {
+      width: 16px;
+      height: 16px;
+    }
+  }
+}
+
+.tx-active {
+  button {
+    color: #fff;
+    background: #c4bba6;
+  }
+}
+
+.signContent {
+  background-color: #f8f8f8;
+  padding: 20px;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  border-radius: 20px 20px 0 0;
+
+  .scroll {
+    // background-color: #fff;
+    padding: 4px;
+    height: 300px;
+    overflow-y: hidden;
+    border: 1px solid #dfdfdf;
+  }
+
+  .footer {
+    margin-top: 10px;
+    color: #fff;
+    padding: 4px 20px;
+    border-radius: 20px;
+    background: linear-gradient(to right, #8ed187, #5dd665);
+  }
+}
+</style>

+ 355 - 0
pages/users/vault/storeMetal/order.vue

@@ -0,0 +1,355 @@
+<template>
+  <view class="list-page">
+    <view class="tabs-box">
+      <up-tabs :list="list" @click="tabsChange" lineColor="#f8c20f"></up-tabs>
+    </view>
+    <view v-if="orderList.length === 0" class="empty">
+      <image
+        style="width: 60%"
+        src="https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/common/empty.png"
+        mode="widthFix"
+      ></image>
+      <text>暂无订单~</text>
+    </view>
+    <view v-else class="inner">
+      <view
+        v-for="(item, index) in orderList"
+        :key="index"
+        class="block"
+        @click="nativeTo(item)"
+      >
+        <view class="header">
+          <view class="title">订单号:{{ item.orderNo }}</view>
+          <view class="tag" :class="['status' + item.status]">
+            {{ getOrderType(item.status) }}
+          </view>
+        </view>
+        <view class="detail">
+          <image
+            style="width: 50px; height: 50px; border-radius: 6px"
+            :src="item.images[0] || emptyImg"
+            mode="scaleToFill"
+            v-if="item.images"
+            @click="previewImage(item.images)"
+          ></image>
+        </view>
+
+        <view class="end">
+          <view>
+            <view class="desc">
+              订单自估重量:
+              <span
+                class="price"
+                v-for="(i, index) in item.goldMaterials"
+                :key="index"
+                >{{ `${getCate(i.type)}${i.weight}g` }}</span
+              >
+            </view>
+          </view>
+          <view class="desc">
+            <view class="">下单时间:{{ item.createTime }}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import {
+  onLoad,
+  onShow,
+  onPullDownRefresh,
+  onReachBottom,
+} from "@dcloudio/uni-app";
+import { depositPageAPI } from "@/api/functions";
+import { useAppStore } from "@/stores/app";
+const appStore = useAppStore();
+// 响应式变量(替代 Vue2 的 data)
+const list = ref([
+  { name: "全部", status: "" },
+  { name: "待签收", status: 0 },
+  { name: "待检测", status: 1 },
+  { name: "待确认", status: 2 },
+  { name: "已完成", status: 4 },
+]);
+const options = ref({ status: 0 });
+const orderList = ref([]);
+const emptyImg = ref(
+  "https://mp-ad17e5cd-05c1-4df9-b060-556e25dac130.cdn.bspapp.com/mini/recycle/example/example1.png"
+);
+
+const params = ref({
+  page: 1,
+  limit: 20,
+  status: "",
+  // userId: appStorze.userInfo.userId,
+});
+const total = ref(0);
+const loading = ref(false); // 加载状态
+const hasMore = ref(true); // 是否还有更多数据
+
+onShow((options) => {
+  console.log(1111);
+  params.value.page = 1;
+  getOrderList();
+});
+const getOrderList = async (isRefresh = false) => {
+  if (loading.value) return;
+  loading.value = true;
+  uni.showLoading({
+    title: "加载中",
+  });
+  try {
+    const res = await depositPageAPI(params.value);
+    console.log(res);
+
+    const newList = res.data.list.map((v) => {
+      return {
+        ...v,
+        images: JSON.parse(v.expressImage),
+      };
+    });
+
+    if (isRefresh) {
+      orderList.value = newList;
+    } else {
+      // 避免重复添加
+      if (params.value.page === 1) {
+        orderList.value = newList;
+      } else {
+        orderList.value = [...orderList.value, ...newList];
+      }
+    }
+    total.value = res.data.total;
+    // 判断是否还有更多数据
+    hasMore.value = orderList.value.length < total.value;
+    uni.hideLoading();
+  } catch (error) {
+    console.error("获取订单列表失败:", error);
+    uni.showToast({
+      title: "加载失败",
+      icon: "none",
+    });
+  } finally {
+    loading.value = false;
+    // 停止下拉刷新动画
+    if (isRefresh) {
+      uni.stopPullDownRefresh();
+    }
+  }
+};
+// 其他生命周期
+onPullDownRefresh(() => {
+  // 下拉刷新逻辑(保持原有空实现,可根据需求补充)
+});
+
+onReachBottom(() => {
+  // 如果没有更多数据或正在加载中,则不执行
+  if (!hasMore.value || loading.value) return;
+
+  // 增加页码
+  params.value.page++;
+  // 加载下一页数据
+  getOrderList();
+});
+
+onShow(() => {
+  // 页面显示时逻辑(保持原有空实现,可根据需求补充)
+});
+
+// 标签页切换
+const tabsChange = (item) => {
+  params.value.status = item.status;
+  params.value.page = 1;
+  getOrderList();
+};
+// 跳转详情页(原 methods 中的 nativeTo)
+const nativeTo = (item) => {
+  if (item.status === 2 || item.status == 3 || item.status == 4) {
+    uni.navigateTo({
+      url: `/pages/users/vault/storeMetal/gmReport?orderInfo=${encodeURIComponent(
+        JSON.stringify(item)
+      )}`,
+    });
+  }
+};
+
+// 获取商品类别(原 methods 中的 getCate)
+const getCate = (cate) => {
+  switch (cate) {
+    case 1:
+      return "黄金";
+    case 3:
+      return "白银";
+    case 2:
+      return "铂金";
+    case 4:
+      return "K金";
+    default:
+      return ""; // 增加默认值,避免 undefined
+  }
+};
+
+// 获取订单状态文本(原 methods 中的 getOrderType)
+const getOrderType = (status) => {
+  switch (status) {
+    case 0:
+      return "待签收";
+    case 1:
+      return "待检测";
+    case 2:
+      return "待确认";
+    case 3:
+      return "待充值";
+    case 4:
+      return "已完成";
+    default:
+      return "-"; // 增加默认值,避免 undefined
+  }
+};
+const previewImage = (urls) => {
+  uni.previewImage({
+    current: urls[0],
+    urls: urls, // 需要预览的图片URL列表
+    success: () => {},
+    fail: (err) => {
+      console.error("预览图片失败:", err);
+    },
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.list-page {
+  // min-height: 100vh;
+  background: $uni-bg-primary !important;
+}
+
+.tabs-box {
+  width: 100%;
+  height: 75rpx;
+  // background: #ffffff;
+  box-sizing: border-box;
+  ::v-deep .u-tabs__wrapper__nav__item {
+    padding: 0 37rpx;
+  }
+}
+
+.empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: #fff;
+}
+.tabs {
+  z-index: 100;
+  position: sticky;
+}
+.inner {
+  padding: 10px;
+  .footer {
+    border-radius: 5px;
+    border: 0 0 10px 20px;
+    color: #707070;
+    font-size: 12px;
+    padding: 10px;
+    background-color: rgb(252, 247, 230);
+  }
+}
+.block {
+  margin-bottom: 10px;
+  padding-top: 10px;
+  border-radius: 5px;
+  background-color: #fff;
+  border: 1px solid #cecece;
+  .header {
+    padding: 0 10px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #eee;
+    .tag {
+      font-size: 12px;
+      padding: 2px 5px;
+      border-radius: 4px;
+      color: #ff0800;
+      background-color: #ffcece;
+      &.status-1 {
+        color: #555;
+        background-color: #eeeeee;
+      }
+      &.status1 {
+        color: #ff9900;
+        background-color: #ffeccf;
+      }
+      &.status2 {
+        color: #d6006b;
+        background-color: #fdd3e9;
+      }
+      &.status3 {
+        color: #9900ff;
+        background-color: #e2b7ff;
+      }
+      &.status4 {
+        color: rgb(48, 24, 136);
+        background-color: #bbb0fa;
+      }
+      &.status5 {
+        color: #0051ff;
+        background-color: #b7d6ff;
+      }
+      &.status6 {
+        color: #3dac27;
+        background-color: #c8ffb7;
+      }
+    }
+    .title {
+      font-size: 14px;
+    }
+  }
+  .info {
+    flex: 1;
+    width: 100%;
+    font-size: 14px;
+    margin-left: 10px;
+    color: #999;
+    .cartList {
+      margin-bottom: 3px;
+      display: flex;
+      justify-content: space-between;
+      .right {
+        min-width: 120px;
+        display: flex;
+        justify-content: space-between;
+        .weight {
+          color: #daa520;
+        }
+      }
+    }
+  }
+  .detail {
+    display: flex;
+    padding: 10px 13px;
+  }
+  .end {
+    padding: 10px 10px;
+    color: #999;
+    font-size: 14px;
+
+    .desc {
+      margin-bottom: 6px;
+      display: flex;
+      align-items: center;
+      // justify-content: space-between;
+
+      .price {
+        margin-right: 10rpx;
+      }
+    }
+  }
+}
+</style>

+ 0 - 0
pages/users/vault/withdraw.vue


BIN
static/avator.png


+ 51 - 0
uni_modules/z-paging/changelog.md

@@ -0,0 +1,51 @@
+## 2.8.7(2025-05-30)
+1.`新增` props:`layout-only`,支持仅使用基础布局。  
+2.`新增` `goF2`方法,支持手动触发进入二楼。  
+3.`新增` `@scrollDirectionChange`事件,支持监听列表滚动方向改变。  
+4.`新增` props:`paging-class`,支持直接设置`z-paging`的`class`。  
+5.`新增` `addKeyboardHeightChangeListener`方法,支持手动添加键盘高度变化监听。  
+6.`修复` `scrollIntoViewById`方法在存在`slot=top`或局部区域滚动时,滚动的位置不准确的问题。  
+7.`优化` 重构底部安全区域处理逻辑,修改为占位view的方式,处理方案更灵活并支持自定义底部安全区域颜色。  
+8.`优化` 兼容在`nvue`+`vue3`中使用`waterfall`。  
+9.`优化` 规范`types`中对`style`类型的约束。  
+## 2.8.6(2025-03-17)
+1.`新增` 聊天记录模式流式输出(类似chatGPT回答)演示demo。  
+2.`新增` z-paging及其公共子组件支持`HBuilderX`代码文档提示。  
+3.`新增` props:`virtual-in-swiper-slot`,用以解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在`swiper-item`中存在的无法获取slot插入的cell高度进而导致虚拟列表失败的问题。  
+4.`新增` `@scrolltolower`和@`scrolltoupper`支持nvue。  
+5.`修复` 由`v2.8.1`引出的方法`scrollIntoViewById`在微信小程序+vue3中无效的问题。  
+6.`修复` 由`v2.8.1`引出的在子组件内使用z-paging虚拟列表无效的问题。  
+7.`修复` 在微信小程序中基础库版本较高时`wx.getSystemInfoSync is deprecated`警告。  
+8.`优化` 提升下拉刷新在鸿蒙Next中的性能。  
+9.`优化` `@scrolltolower`和`@scrolltoupper`在倒置的聊天记录模式下的触发逻辑。  
+10.`优化` 其他细节调整。  
+## 2.8.5(2025-02-09)
+1.`新增` 方法`scrollToX`,支持控制x轴滚动到指定位置。  
+2.`修复` 快手小程序中报错`await isn't allowed in non-async function`的问题。  
+3.`修复` 在iOS+nvue中,设置了`:loading-more-enabled="false"`后,调用`scrollToBottom`无法滚动到底部的问题。  
+4.`修复` 在支付宝小程序+页面滚动中,数据为空时空数据图未居中的问题。  
+5.`优化` fetch types修改。  
+## 2.8.4(2024-12-02)
+1.`修复` 在虚拟列表+vue2中,顶部占位采用transformY方案;在虚拟列表+vue3中,顶部占位采用view占位方案。以解决在vue2+微信小程序+安卓+兼容模式中,可能出现的虚拟列表闪动的问题。  
+2.`修复` 在列表渲染时(尤其是在虚拟列表中)偶现的【点击加载更多】闪现的问题。   
+3.`优化` 统一在RefresherStatus枚举中Loading取值。    
+4.`优化` `defaultPageNo`&`defaultPageSize`修改为只允许number类型。  
+5.`优化` 提升兼容性&细节优化。  
+## 2.8.3(2024-11-27)
+1.`修复` `doInsertVirtualListItem`插入数据无效的问题。  
+2.`优化` 提升兼容性&细节优化。  
+## 2.8.2(2024-11-25)
+1.`优化` types中`ZPagingRef`和`ZPagingInstance`支持泛型。  
+## 2.8.1(2024-11-24)
+1.`新增` 完整的`props`、`slots`、`methods`、`events`的typescript types声明,可在ts中获得绝佳的代码提示体验。  
+2.`新增` `virtual-cell-id-prefix`:虚拟列表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id。  
+3.`修复` 在vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若`z-paging`在`swiper-item`标签内的情况下存在的无法获取slot插入的cell高度的问题。  
+4.`修复` 在虚拟列表中分页数据小于1页时插入新数据,虚拟列表未生效的问题。  
+5.`修复` 在虚拟列表中调用`refresh`时,cell的index计算不正确的问题。  
+6.`修复` 在快手小程序中内容较少或空数据时`z-paging`未能铺满全屏的问题。  
+7.`优化` `events`中的参数涉及枚举的部分,统一由之前的number类型修改为string类型,展示更直观!涉及的events:`@query`中的`from`参数;`@refresherStatusChange`中的`status`参数;`@loadingStatusChange`中的`status`参数;`slot=refresher`中的`refresherStatus`参数;`slot=chatLoading`中的`loadingMoreStatus`参数。更新版本请特别留意!  
+## 2.8.0(2024-10-21)
+1.`新增` 全面支持鸿蒙Next。  
+2.`修复` 设置了`refresher-complete-delay`后,在下拉刷新期间调用reload导致的无法再次下拉刷新的问题。  
+3.`优化` 废弃虚拟列表transformY顶部占位方案,修改为空view占位。解决因使用旧方案导致的vue3中可能出现的虚拟列表闪动问题。提升虚拟列表的兼容性。  
+

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

@@ -0,0 +1,47 @@
+<!-- z-paging -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
+<!-- 反馈QQ群:790460711 -->
+
+<!-- z-paging-cell,用于在nvue中使用cell包裹,vue中使用view包裹 -->
+<template>
+	<!-- #ifdef APP-NVUE -->
+	<cell :style="[cellStyle]" @touchstart="onTouchstart">
+		<slot />
+	</cell>
+	<!-- #endif -->
+	<!-- #ifndef APP-NVUE -->
+	<view :style="[cellStyle]" @touchstart="onTouchstart">
+		<slot />
+	</view>
+	<!-- #endif -->
+</template>
+
+<script>
+	/**
+	 * z-paging-cell 组件
+	 * @description 用于兼容 nvue 和 vue 中的 cell 渲染。因为在 nvue 中 z-paging 内置的是 list,因此列表 item 必须使用 cell 包住;在 vue 中不能使用 cell,否则会报组件找不到的错误。此子组件为了兼容这两种情况,内部作了条件编译处理。
+	 * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-cell配置
+	 * @notice 以下为 z-paging-cell 的配置项
+	 * @property {Object} cellStyle cell 样式,默认为 {}
+	 * @example <z-paging-cell :cellStyle="{ backgroundColor: '#f0f0f0' }"></z-paging-cell>
+	 */
+	export default {
+		name: "z-paging-cell",
+		props: {
+			//cellStyle
+			cellStyle: {
+				type: Object,
+				default: function() {
+                    return {}
+                }
+			}
+		},
+		methods: {
+			onTouchstart(e) {
+				this.$emit('touchstart', e);
+			}
+		}
+	}
+</script>
+

+ 209 - 0
uni_modules/z-paging/components/z-paging-empty-view/z-paging-empty-view.vue

@@ -0,0 +1,209 @@
+<!-- z-paging -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
+<!-- 反馈QQ群:790460711 -->
+
+<!-- 空数据占位view,此组件支持easycom规范,可以在项目中直接引用 -->
+<template>
+	<view :class="{'zp-container':true,'zp-container-fixed':emptyViewFixed}" :style="[finalEmptyViewStyle]" @click="emptyViewClick">
+		<view class="zp-main">
+			<image v-if="!emptyViewImg.length" :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" :style="[emptyViewImgStyle]" :src="emptyImg" />
+			<image v-else :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg" />
+			<text class="zp-main-title" :class="{'zp-main-title-rpx':unit==='rpx','zp-main-title-px':unit==='px'}" :style="[emptyViewTitleStyle]">{{emptyViewText}}</text>
+			<text v-if="showEmptyViewReload" :class="{'zp-main-error-btn':true,'zp-main-error-btn-rpx':unit==='rpx','zp-main-error-btn-px':unit==='px'}" :style="[emptyViewReloadStyle]" @click.stop="reloadClick">{{emptyViewReloadText}}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	import zStatic from '../z-paging/js/z-paging-static'
+	
+	/**
+	 * z-paging-empty-view 空数据组件
+	 * @description 通用的 z-paging 空数据组件
+	 * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-empty-view配置
+	 * @property {Boolean} emptyViewFixed 空数据图片是否铺满 z-paging,默认为 false。若设置为 true,则为填充满 z-paging 的剩余部分
+	 * @property {String} emptyViewText 空数据图描述文字,默认为 '没有数据哦~'
+	 * @property {String} emptyViewImg 空数据图图片,默认使用 z-paging 内置的图片 (建议使用绝对路径,开头不要添加 "@",请以 "/" 开头)
+	 * @property {String} emptyViewReloadText 空数据图点击重新加载文字,默认为 '重新加载'
+	 * @property {Object} emptyViewStyle 空数据图样式,可设置空数据 view 的 top 等,如: empty-view-style="{'top':'100rpx'}" (如果空数据图不是 fixed 布局,则此处是 margin-top),默认为 {}
+	 * @property {Object} emptyViewImgStyle 空数据图 img 样式,默认为 {}
+	 * @property {Object} emptyViewTitleStyle 空数据图描述文字样式,默认为 {}
+	 * @property {Object} emptyViewReloadStyle 空数据图重新加载按钮样式,默认为 {}
+	 * @property {Boolean} showEmptyViewReload 是否显示空数据图重新加载按钮(无数据时),默认为 false
+	 * @property {Boolean} isLoadFailed 是否是加载失败,默认为 false
+	 * @property {String} unit 空数据图中布局的单位,默认为 'rpx'
+	 * @event {Function} reload 点击了重新加载按钮
+	 * @event {Function} viewClick 点击了空数据图 view
+	 * @example <z-paging-empty-view empty-view-text="暂无数据" />
+	 */
+	export default {
+		name: "z-paging-empty-view",
+		data() {
+			return {
+				
+			};
+		},
+		props: {
+			// 空数据描述文字
+			emptyViewText: {
+				type: String,
+				default: '没有数据哦~'
+			},
+			// 空数据图片
+			emptyViewImg: {
+				type: String,
+				default: ''
+			},
+			// 是否显示空数据图重新加载按钮
+			showEmptyViewReload: {
+				type: Boolean,
+				default: false
+			},
+			// 空数据点击重新加载文字
+			emptyViewReloadText: {
+				type: String,
+				default: '重新加载'
+			},
+			// 是否是加载失败
+			isLoadFailed: {
+				type: Boolean,
+				default: false
+			},
+			// 空数据图样式
+			emptyViewStyle: {
+				type: Object,
+				default: function() {
+                    return {}
+                }
+			},
+			// 空数据图img样式
+			emptyViewImgStyle: {
+				type: Object,
+				default: function() {
+				    return {}
+				}
+			},
+			// 空数据图描述文字样式
+			emptyViewTitleStyle: {
+				type: Object,
+				default: function() {
+				    return {}
+				}
+			},
+			// 空数据图重新加载按钮样式
+			emptyViewReloadStyle: {
+				type: Object,
+				default: function() {
+				    return {}
+				}
+			},
+			// 空数据图z-index
+			emptyViewZIndex: {
+				type: Number,
+				default: 9
+			},
+			// 空数据图片是否使用fixed布局并铺满z-paging
+			emptyViewFixed: {
+				type: Boolean,
+				default: true
+			},
+			// 空数据图中布局的单位,默认为rpx
+			unit: {
+				type: String,
+				default: 'rpx'
+			}
+		},
+		computed: {
+			emptyImg() {
+                return this.isLoadFailed ? zStatic.base64Error : zStatic.base64Empty;
+			},
+			finalEmptyViewStyle(){
+				this.emptyViewStyle['z-index'] = this.emptyViewZIndex;
+				return this.emptyViewStyle;
+			}
+		},
+		methods: {
+			// 点击了reload按钮
+			reloadClick() {
+				this.$emit('reload');
+			},
+			// 点击了空数据view
+			emptyViewClick() {
+				this.$emit('viewClick');
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.zp-container{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+	}
+	.zp-container-fixed {
+		/* #ifndef APP-NVUE */
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		/* #endif */
+	}
+
+	.zp-main{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		align-items: center;
+        padding: 50rpx 0rpx;
+	}
+
+	.zp-main-image-rpx {
+		width: 240rpx;
+		height: 240rpx;
+	}
+	.zp-main-image-px {
+		width: 120px;
+		height: 120px;
+	}
+
+	.zp-main-title {
+		color: #aaaaaa;
+		text-align: center;
+	}
+	.zp-main-title-rpx {
+		font-size: 28rpx;
+		margin-top: 10rpx;
+		padding: 0rpx 20rpx;
+	}
+	.zp-main-title-px {
+		font-size: 14px;
+		margin-top: 5px;
+		padding: 0px 10px;
+	}
+
+	.zp-main-error-btn {
+		border: solid 1px #dddddd;
+		color: #aaaaaa;
+	}
+	.zp-main-error-btn-rpx {
+		font-size: 28rpx;
+		padding: 8rpx 24rpx;
+		border-radius: 6rpx;
+		margin-top: 50rpx;
+	}
+	.zp-main-error-btn-px {
+		font-size: 14px;
+		padding: 4px 12px;
+		border-radius: 3px;
+		margin-top: 25px;
+	}
+</style>

+ 160 - 0
uni_modules/z-paging/components/z-paging-swiper-item/z-paging-swiper-item.vue

@@ -0,0 +1,160 @@
+<!-- z-paging -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
+<!-- 反馈QQ群:790460711 -->
+
+<!-- 滑动切换选项卡swiper-item,此组件支持easycom规范,可以在项目中直接引用 -->
+<template>
+	<view class="zp-swiper-item-container">
+		<z-paging ref="paging" :fixed="false" 
+			:auto="false" :useVirtualList="useVirtualList" :useInnerList="useInnerList" :cellKeyName="cellKeyName" :innerListStyle="innerListStyle" 
+			:preloadPage="preloadPage" :cellHeightMode="cellHeightMode" :virtualScrollFps="virtualScrollFps" :virtualListCol="virtualListCol"
+			@query="_queryList" @listChange="_updateList">
+			<slot />
+			<template #header>
+				<slot name="header"/>
+			</template>
+			<template #cell="{item,index}">
+				<slot name="cell" :item="item" :index="index"/>
+			</template>
+			<template #footer>
+				<slot name="footer"/>
+			</template>
+		</z-paging>
+	</view>
+</template>
+
+<script>
+	import zPaging from '../z-paging/z-paging'
+	/**
+	 * z-paging-swiper-item 组件
+	 * @description swiper+list极简写法中使用到,实际上就是对普通的swiper+list中swiper-item的包装封装,用以简化写法,但个性化配置局限较多
+	 * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper-item配置
+	 * @notice 以下为 z-paging-swiper-item 的配置项
+	 * @property {Number} tabIndex 当前组件的 index,也就是当前组件是 swiper 中的第几个,默认为 0
+	 * @property {Number} currentIndex 当前 swiper 切换到第几个 index,默认为 0
+	 * @property {Boolean} useVirtualList 是否使用虚拟列表,默认为 false
+	 * @property {Boolean} useInnerList 是否在 z-paging 内部循环渲染列表(内置列表),默认为 false。若 useVirtualList 为 true,则此项恒为 true
+	 * @property {String} cellKeyName 内置列表 cell 的 key 名称,仅 nvue 有效,在 nvue 中开启 useInnerList 时必须填此项,默认为 ''
+	 * @property {Object} innerListStyle innerList 样式,默认为 {}
+	 * @property {Number|String} preloadPage 预加载的列表可视范围(列表高度)页数,默认为 12。此数值越大,则虚拟列表中加载的 dom 越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
+	 * @property {String} cellHeightMode 虚拟列表 cell 高度模式,默认为 'fixed',也就是每个 cell 高度完全相同,将以第一个 cell 高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】
+	 * @property {Number|String} virtualListCol 虚拟列表列数,默认为 1。常用于每行有多列的情况,例如每行有 2 列数据,需要将此值设置为 2
+	 * @property {Number|String} virtualScrollFps 虚拟列表 scroll 取样帧率,默认为 60,过高可能出现卡顿等问题
+	 * @example <z-paging-swiper-item ref="swiperItem" :tabIndex="index" :currentIndex="current" @query="queryList" @updateList="updateList"></z-paging-swiper-item>
+	 */
+	export default {
+		name: "z-paging-swiper-item",
+		components: { zPaging },
+		data() {
+			return {
+				firstLoaded: false
+			}
+		},
+		props: {
+			// 当前组件的index,也就是当前组件是swiper中的第几个
+			tabIndex: {
+				type: Number,
+				default: function() {
+					return 0
+				}
+			},
+			// 当前swiper切换到第几个index
+			currentIndex: {
+				type: Number,
+				default: function() {
+					return 0
+				}
+			},
+			// 是否使用虚拟列表,默认为否
+			useVirtualList: {
+				type: Boolean,
+				default: false
+			},
+			// 是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
+			useInnerList: {
+				type: Boolean,
+				default: false
+			},
+			// 内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
+			cellKeyName: {
+				type: String,
+				default: ''
+			},
+			// innerList样式
+			innerListStyle: {
+				type: Object,
+				default: function() {
+					return {};
+				}
+			},
+			// 预加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
+			preloadPage: {
+				type: [Number, String],
+				default: 12
+			},
+			// 虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
+			cellHeightMode: {
+				type: String,
+				default: 'fixed'
+			},
+			// 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
+			virtualListCol: {
+				type: [Number, String],
+				default: 1
+			},
+			// 虚拟列表scroll取样帧率,默认为60,过高可能出现卡顿等问题
+			virtualScrollFps: {
+				type: [Number, String],
+				default: 60
+			},
+		},
+		watch: {
+			currentIndex: {
+				handler(newVal, oldVal) {
+					if (newVal === this.tabIndex) {
+						// 懒加载,当滑动到当前的item时,才去加载
+						if (!this.firstLoaded) {
+							this.$nextTick(()=>{
+								let delay = 5;
+								// #ifdef MP-TOUTIAO
+								delay = 100;
+								// #endif
+								setTimeout(() => {
+									this.$refs.paging.reload().catch(() => {});
+								}, delay);
+							})
+						}
+					}
+				},
+				immediate: true
+			}
+		},
+		methods: {
+			reload(data) {
+				return this.$refs.paging.reload(data);
+			},
+			complete(data) {
+				this.firstLoaded = true;
+				return this.$refs.paging.complete(data);
+			},
+			_queryList(pageNo, pageSize, from) {
+				this.$emit('query', pageNo, pageSize, from);
+			},
+			_updateList(list) {
+				this.$emit('updateList', list);
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.zp-swiper-item-container {
+		/* #ifndef APP-NVUE */
+		height: 100%;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		/* #endif */
+	}
+</style>

+ 176 - 0
uni_modules/z-paging/components/z-paging-swiper/z-paging-swiper.vue

@@ -0,0 +1,176 @@
+<!-- z-paging -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
+<!-- 反馈QQ群:790460711 -->
+
+<!-- 滑动切换选项卡swiper容器,此组件支持easycom规范,可以在项目中直接引用 -->
+<template>
+	<view :class="fixed?'zp-swiper-container zp-swiper-container-fixed':'zp-swiper-container'" :style="[finalSwiperStyle]">
+		<!-- #ifndef APP-PLUS -->
+		<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
+		<!-- #endif -->
+		<slot v-if="zSlots.top" name="top" />
+		<view class="zp-swiper-super">
+			<view v-if="zSlots.left" :class="{'zp-swiper-left':true,'zp-absoulte':isOldWebView}">
+				<slot name="left" />
+			</view>
+			<view :class="{'zp-swiper':true,'zp-absoulte':isOldWebView}" :style="[swiperContentStyle]">
+				<slot />
+			</view>
+			<view v-if="zSlots.right" :class="{'zp-swiper-right':true,'zp-absoulte zp-right':isOldWebView}">
+				<slot name="right" />
+			</view>
+		</view>
+		<slot v-if="zSlots.bottom" name="bottom" />
+	</view>
+</template>
+
+<script>
+	import commonLayoutModule from '../z-paging/js/modules/common-layout'
+	
+	/**
+	 * z-paging-swiper 组件
+	 * @description 在 swiper 中使用 z-paging 时(左右滑动切换列表),在根节点使用 z-paging-swiper,其相当于一个 view 容器,默认铺满全屏,可免计算高度直接插入 swiper 的视图容器。
+	 * @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper配置
+	 * @property {Boolean} fixed 是否使用 fixed 布局,默认为 true
+	 * @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认为 false
+	 * @property {Object} swiperStyle z-paging-swiper 样式,默认为 {}
+	 * @example <z-paging-swiper :safeAreaInsetBottom="true"></z-paging-swiper>
+	 */
+	export default {
+		name: "z-paging-swiper",
+		mixins: [commonLayoutModule],
+		data() {
+			return {
+				swiperContentStyle: {}
+			};
+		},
+		props: {
+			// 是否使用fixed布局,默认为是
+			fixed: {
+				type: Boolean,
+				default: true
+			},
+			// 是否开启底部安全区域适配
+			safeAreaInsetBottom: {
+				type: Boolean,
+				default: false
+			},
+			// z-paging-swiper样式
+			swiperStyle: {
+				type: Object,
+				default: function() {
+					return {};
+				},
+			}
+		},
+		mounted() {
+			this.$nextTick(() => {
+				this.systemInfo = this._getSystemInfoSync();
+				setTimeout(this.updateFixedLayout, 100)
+			})
+			// #ifndef APP-PLUS
+			this._getCssSafeAreaInsetBottom();
+			// #endif
+			this.updateLeftAndRightWidth();
+
+			this.swiperContentStyle = { 'flex': '1' };
+			// #ifndef APP-NVUE
+			this.swiperContentStyle = { width: '100%',height: '100%' };
+			// #endif
+		},
+		computed: {
+			finalSwiperStyle() {
+				const swiperStyle = { ...this.swiperStyle };
+				if (!this.systemInfo) return swiperStyle;
+				const windowTop = this.windowTop;
+				const windowBottom = this.systemInfo.windowBottom;
+				if (this.fixed) {
+					if (windowTop && !swiperStyle.top) {
+						swiperStyle.top = windowTop + 'px';
+					}
+					if (!swiperStyle.bottom) {
+						let bottom = windowBottom || 0;
+						bottom += this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
+						if (bottom > 0) {
+							swiperStyle.bottom = bottom + 'px';
+						}
+					}
+				}
+				return swiperStyle;
+			}
+		},
+		methods: {
+			// 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
+			updateLeftAndRightWidth() {
+				if (!this.isOldWebView) return;
+				this.$nextTick(() => this._updateLeftAndRightWidth(this.swiperContentStyle, 'zp-swiper'));
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.zp-swiper-container {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		flex: 1;
+	}
+
+	.zp-swiper-container-fixed {
+		position: fixed;
+		/* #ifndef APP-NVUE */
+		height: auto;
+		width: auto;
+		/* #endif */
+		top: 0;
+		left: 0;
+		bottom: 0;
+		right: 0;
+	}
+	
+	.zp-safe-area-inset-bottom {
+		position: absolute;
+		/* #ifndef APP-PLUS */
+		height: env(safe-area-inset-bottom);
+		/* #endif */
+	}
+
+	.zp-swiper-super {
+		flex: 1;
+		overflow: hidden;
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+	
+	.zp-swiper-left,.zp-swiper-right{
+		/* #ifndef APP-NVUE */
+		height: 100%;
+		/* #endif */
+	}
+
+	.zp-swiper {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		height: 100%;
+		width: 100%;
+		/* #endif */
+	}
+	
+	.zp-absoulte {
+		/* #ifndef APP-NVUE */
+		position: absolute;
+		top: 0;
+		width: auto;
+		/* #endif */
+	}
+	
+	.zp-swiper-item {
+		height: 100%;
+	}
+</style>

+ 182 - 0
uni_modules/z-paging/components/z-paging/components/z-paging-load-more.vue

@@ -0,0 +1,182 @@
+<!-- [z-paging]上拉加载更多view -->
+<template>
+	<view class="zp-l-container" :class="{'zp-l-container-rpx':c.unit==='rpx','zp-l-container-px':c.unit==='px'}" :style="[c.customStyle]" @click="doClick">
+		<template v-if="!c.hideContent">
+			<!-- 底部加载更多没有更多数据分割线 -->
+			<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
+			<!-- 底部加载更多loading -->
+			<!-- #ifndef APP-NVUE -->
+			<image v-if="finalStatus===M.Loading&&!!c.loadingIconCustomImage"
+				:src="c.loadingIconCustomImage" :style="[c.iconCustomStyle]" :class="{'zp-l-line-loading-custom-image':true,'zp-l-line-loading-custom-image-animated':c.loadingAnimated,'zp-l-line-loading-custom-image-rpx':c.unit==='rpx','zp-l-line-loading-custom-image-px':c.unit==='px'}" />
+			<image v-if="finalStatus===M.Loading&&finalLoadingIconType==='flower'&&!c.loadingIconCustomImage.length"
+				:class="{'zp-line-loading-image':true,'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[c.iconCustomStyle]" :src="zTheme.flower[ts]" />
+			<!-- #endif -->
+			<!-- #ifdef APP-NVUE -->
+			<!-- 在nvue中底部加载更多loading使用系统自带的 -->
+			<view>
+				<loading-indicator v-if="finalStatus===M.Loading&&finalLoadingIconType!=='circle'" :class="{'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[{color:zTheme.indicator[ts]}]" :animating="true" />
+			</view>
+			<!-- #endif -->
+			<!-- 底部加载更多文字 -->
+			<text v-if="finalStatus===M.Loading&&finalLoadingIconType==='circle'&&!c.loadingIconCustomImage.length"
+				class="zp-l-circle-loading-view" :class="{'zp-l-circle-loading-view-rpx':c.unit==='rpx','zp-l-circle-loading-view-px':c.unit==='px'}" :style="[{borderColor:zTheme.circleBorder[ts],borderTopColor:zTheme.circleBorderTop[ts]},c.iconCustomStyle]" />
+			<text v-if="!c.isChat||(!c.chatDefaultAsLoading&&finalStatus===M.Default)||finalStatus===M.Fail" :class="{'zp-l-text-rpx':c.unit==='rpx','zp-l-text-px':c.unit==='px'}" :style="[{color:zTheme.title[ts]},c.titleCustomStyle]">{{ownLoadingMoreText}}</text>
+			<!-- 底部加载更多没有更多数据分割线 -->
+			<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
+		</template>
+	</view>
+</template>
+<script>
+	import zStatic from '../js/z-paging-static'
+	import Enum from '../js/z-paging-enum'
+	export default {
+		name: 'z-paging-load-more',
+		data() {
+			return {
+				M: Enum.More,
+				zTheme: {
+					title: { white: '#efefef', black: '#a4a4a4' },
+					line: { white: '#efefef', black: '#eeeeee' },
+					circleBorder: { white: '#aaaaaa', black: '#c8c8c8' },
+					circleBorderTop: { white: '#ffffff', black: '#444444' },
+					flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
+					indicator: { white: '#eeeeee', black: '#777777' }
+				}
+			};
+		},
+		props: ['zConfig'],
+		computed: {
+			ts() {
+				return this.c.defaultThemeStyle;
+			},
+			// 底部加载更多配置
+			c() {
+				return this.zConfig || {};
+			},
+			// 底部加载更多文字
+			ownLoadingMoreText() {
+				return {
+				    [this.M.Default]: this.c.defaultText,
+				    [this.M.Loading]: this.c.loadingText,
+				    [this.M.NoMore]: this.c.noMoreText,
+				    [this.M.Fail]: this.c.failText,
+				}[this.finalStatus];
+			},
+			// 底部加载更多状态
+			finalStatus() {
+				if (this.c.defaultAsLoading && this.c.status === this.M.Default) return this.M.Loading;
+				return this.c.status;
+			},
+			// 加载更多icon类型
+			finalLoadingIconType() {
+				// #ifdef APP-NVUE
+				return 'flower';
+				// #endif
+				return this.c.loadingIconType;
+			}
+		},
+		methods: {
+			// 点击了加载更多
+			doClick() {
+				this.$emit('doClick');
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@import "../css/z-paging-static.css";
+
+	.zp-l-container {
+		/* #ifndef APP-NVUE */
+		clear: both;
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+	}
+	.zp-l-container-rpx {
+		height: 80rpx;
+		font-size: 27rpx;
+	}
+	.zp-l-container-px {
+		height: 40px;
+		font-size: 14px;
+	}
+
+	.zp-l-line-loading-custom-image {
+		color: #a4a4a4;
+	}
+	.zp-l-line-loading-custom-image-rpx {
+		margin-right: 8rpx;
+		width: 28rpx;
+		height: 28rpx;
+	}
+	.zp-l-line-loading-custom-image-px {
+		margin-right: 4px;
+		width: 14px;
+		height: 14px;
+	}
+	
+	.zp-l-line-loading-custom-image-animated{
+		/* #ifndef APP-NVUE */
+		animation: loading-circle 1s linear infinite;
+		/* #endif */
+	}
+
+	.zp-l-circle-loading-view {
+		border: 3rpx solid #dddddd;
+		border-radius: 50%;
+		/* #ifndef APP-NVUE */
+		animation: loading-circle 1s linear infinite;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 30rpx;
+		height: 30rpx;
+		/* #endif */
+	}
+	.zp-l-circle-loading-view-rpx {
+		margin-right: 8rpx;
+		width: 23rpx;
+		height: 23rpx;
+	}
+	.zp-l-circle-loading-view-px {
+		margin-right: 4px;
+		width: 12px;
+		height: 12px;
+	}
+
+	.zp-l-text-rpx {
+		font-size: 30rpx;
+		margin: 0rpx 6rpx;
+	}
+	.zp-l-text-px {
+		font-size: 15px;
+		margin: 0px 3px;
+	}
+
+	.zp-l-line-rpx {
+		height: 1px;
+		width: 100rpx;
+		margin: 0rpx 10rpx;
+	}
+	.zp-l-line-px {
+		height: 1px;
+		width: 50px;
+		margin: 0rpx 5px;
+	}
+
+	/* #ifndef APP-NVUE */
+	@keyframes loading-circle {
+		0% {
+			-webkit-transform: rotate(0deg);
+			transform: rotate(0deg);
+		}
+		100% {
+			-webkit-transform: rotate(360deg);
+			transform: rotate(360deg);
+		}
+	}
+	/* #endif */
+</style>

+ 214 - 0
uni_modules/z-paging/components/z-paging/components/z-paging-refresh.vue

@@ -0,0 +1,214 @@
+<!-- [z-paging]下拉刷新view -->
+<template>
+	<view style="height: 100%;">
+		<view :class="showUpdateTime?'zp-r-container zp-r-container-padding':'zp-r-container'">
+			<view class="zp-r-left">
+				<!-- 非加载中(继续下拉刷新、松手立即刷新状态图片) -->
+				<image v-if="status!==R.Loading" :class="leftImageClass" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
+				<!-- 加载状态图片 -->
+				<!-- #ifndef APP-NVUE -->
+				<image v-else :class="{'zp-line-loading-image':refreshingAnimated,'zp-r-left-image':true,'zp-r-left-image-pre-size-rpx':unit==='rpx','zp-r-left-image-pre-size-px':unit==='px'}" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
+				<!-- #endif -->
+				<!-- 在nvue中,加载状态loading使用系统loading -->
+				<!-- #ifdef APP-NVUE -->
+				<view v-else :style="[{'margin-right':showUpdateTime?addUnit(18,unit):addUnit(12, unit)}]">
+					<loading-indicator :class="isIos?{'zp-loading-image-ios-rpx':unit==='rpx','zp-loading-image-ios-px':unit==='px'}:{'zp-loading-image-android-rpx':unit==='rpx','zp-loading-image-android-px':unit==='px'}" 
+					:style="[{color:zTheme.indicator[ts]},imgStyle]" :animating="true" />
+				</view>
+				<!-- #endif -->
+			</view>
+			<!-- 右侧文字内容 -->
+			<view class="zp-r-right">
+				<!-- 右侧下拉刷新状态文字 -->
+				<text class="zp-r-right-text" :style="[rightTextStyle,titleStyle]">{{currentTitle}}</text>
+				<!-- 右侧下拉刷新时间文字 -->
+				<text v-if="showUpdateTime&&refresherTimeText.length" class="zp-r-right-text" :class="{'zp-r-right-time-text-rpx':unit==='rpx','zp-r-right-time-text-px':unit==='px'}" :style="[{color:zTheme.title[ts]},updateTimeStyle]">
+					{{refresherTimeText}}
+				</text>
+			</view>
+		</view>
+	</view>
+</template>
+<script>
+	import zStatic from '../js/z-paging-static'
+	import u from '../js/z-paging-utils'
+	import Enum from '../js/z-paging-enum'
+	
+	export default {
+		name: 'z-paging-refresh',
+		data() {
+			return {
+				R: Enum.Refresher,
+				refresherTimeText: '',
+				zTheme: {
+					title: { white: '#efefef', black: '#555555' },
+					arrow: { white: zStatic.base64ArrowWhite, black: zStatic.base64Arrow },
+					flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
+					success: { white: zStatic.base64SuccessWhite, black: zStatic.base64Success },
+					indicator: { white: '#eeeeee', black: '#777777' }
+				}
+			};
+		},
+		props: ['status', 'defaultThemeStyle', 'defaultText', 'pullingText', 'refreshingText', 'completeText', 'goF2Text', 'defaultImg', 'pullingImg', 
+			'refreshingImg', 'completeImg', 'refreshingAnimated', 'showUpdateTime', 'updateTimeKey', 'imgStyle', 'titleStyle', 'updateTimeStyle', 'updateTimeTextMap', 'unit', 'isIos'
+		],
+		computed: {
+			ts() {
+				return this.defaultThemeStyle;
+			},
+			// 当前状态Map
+			statusTextMap() {
+				this.updateTime();
+				const { R, defaultText, pullingText, refreshingText, completeText, goF2Text } = this;
+				return {
+					[R.Default]: defaultText,
+					[R.ReleaseToRefresh]: pullingText,
+					[R.Loading]: refreshingText,
+					[R.Complete]: completeText,
+					[R.GoF2]: goF2Text,
+				};
+			},
+			// 当前状态文字
+			currentTitle() {
+				return this.statusTextMap[this.status] || this.defaultText;
+			},
+			// 左侧图片class
+			leftImageClass() {
+				const preSizeClass = `zp-r-left-image-pre-size-${this.unit}`;
+				if (this.status === this.R.Complete) return preSizeClass;
+				return `zp-r-left-image ${preSizeClass} ${this.status === this.R.Default ? 'zp-r-arrow-down' : 'zp-r-arrow-top'}`;
+			},
+			// 左侧图片style
+			leftImageStyle() {
+				const showUpdateTime = this.showUpdateTime;
+				const size = showUpdateTime ? u.addUnit(36, this.unit) : u.addUnit(34, this.unit);
+				return {width: size,height: size,'margin-right': showUpdateTime ? u.addUnit(20, this.unit) : u.addUnit(9, this.unit)};
+			},
+			// 左侧图片src
+			leftImageSrc() {
+				const R = this.R;
+				const status = this.status;
+				if (status === R.Default) {
+					if (!!this.defaultImg) return this.defaultImg;
+					return this.zTheme.arrow[this.ts];
+				} else if (status === R.ReleaseToRefresh) {
+					if (!!this.pullingImg) return this.pullingImg;
+					if (!!this.defaultImg) return this.defaultImg;
+					return this.zTheme.arrow[this.ts];
+				} else if (status === R.Loading) {
+					if (!!this.refreshingImg) return this.refreshingImg;
+					return this.zTheme.flower[this.ts];;
+				} else if (status === R.Complete) {
+					if (!!this.completeImg) return this.completeImg;
+					return this.zTheme.success[this.ts];;
+				} else if (status === R.GoF2) {
+					return this.zTheme.arrow[this.ts];
+				}
+				return '';
+			},
+			// 右侧文字style
+			rightTextStyle() {
+				let stl = {};
+				// #ifdef APP-NVUE
+				const textHeight = this.showUpdateTime ? u.addUnit(40, this.unit) : u.addUnit(80, this.unit);
+				stl = {'height': textHeight, 'line-height': textHeight}
+				// #endif
+				stl['color'] = this.zTheme.title[this.ts];
+				stl['font-size'] = u.addUnit(30, this.unit);
+				return stl;
+			}
+		},
+		methods: {
+			// 添加单位
+			addUnit(value, unit) {
+				return u.addUnit(value, unit);
+			},
+			// 更新下拉刷新时间
+			updateTime() {
+				if (this.showUpdateTime) {
+					this.refresherTimeText = u.getRefesrherFormatTimeByKey(this.updateTimeKey, this.updateTimeTextMap);
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@import "../css/z-paging-static.css";
+
+	.zp-r-container {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		height: 100%;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.zp-r-container-padding {
+		/* #ifdef APP-NVUE */
+		padding: 7px 0rpx;
+		/* #endif */
+	}
+
+	.zp-r-left {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		overflow: hidden;
+		/* #ifdef MP-ALIPAY */
+		margin-top: -4rpx;
+		/* #endif */
+	}
+
+	.zp-r-left-image {
+		transition-duration: .2s;
+		transition-property: transform;
+		color: #666666;
+	}
+	
+	.zp-r-left-image-pre-size-rpx {
+		/* #ifndef APP-NVUE */
+		width: 34rpx;
+		height: 34rpx;
+		overflow: hidden;
+		/* #endif */
+	}
+	
+	.zp-r-left-image-pre-size-px {
+		/* #ifndef APP-NVUE */
+		width: 17px;
+		height: 17px;
+		overflow: hidden;
+		/* #endif */
+	}
+
+	.zp-r-arrow-top {
+		transform: rotate(0deg);
+	}
+
+	.zp-r-arrow-down {
+		transform: rotate(180deg);
+	}
+
+	.zp-r-right {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.zp-r-right-time-text-rpx {
+		margin-top: 10rpx;
+		font-size: 26rpx;
+	}
+	.zp-r-right-time-text-px {
+		margin-top: 5px;
+		font-size: 13px;
+	}
+</style>

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

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

+ 241 - 0
uni_modules/z-paging/components/z-paging/css/z-paging-main.css

@@ -0,0 +1,241 @@
+/* [z-paging]公共css*/
+
+.z-paging-content {
+	position: relative;
+	flex-direction: column;
+	/* #ifndef APP-NVUE */
+	overflow: hidden;
+	/* #endif */
+}
+
+.z-paging-content-full {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	width: 100%;
+	height: 100%;
+	/* #endif */
+}
+
+.z-paging-content-fixed, .zp-loading-fixed {
+	position: fixed;
+	/* #ifndef APP-NVUE */
+	height: auto;
+	width: auto;
+	/* #endif */
+	top: 0;
+	left: 0;
+	bottom: 0;
+	right: 0;
+}
+
+.zp-f2-content {
+	width: 100%;
+	position: fixed;
+	top: 0;
+	left: 0;
+	background-color: white;
+}
+
+.zp-page-top, .zp-page-bottom {
+	/* #ifndef APP-NVUE */
+	width: auto;
+	/* #endif */
+	position: fixed;
+	left: 0;
+	right: 0;
+	z-index: 999;
+}
+
+.zp-page-left, .zp-page-right {
+	/* #ifndef APP-NVUE */
+	height: 100%;
+	/* #endif */
+}
+
+.zp-scroll-view-super {
+	flex: 1;
+	overflow: hidden;
+	position: relative;
+}
+
+.zp-view-super {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+}
+
+.zp-scroll-view-container, .zp-scroll-view {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	height: 100%;
+	width: 100%;
+	/* #endif */
+}
+
+.zp-absoulte {
+	/* #ifndef APP-NVUE */
+	position: absolute;
+	top: 0;
+	width: auto;
+	/* #endif */
+}
+
+.zp-scroll-view-absolute {
+	position: absolute;
+	top: 0;
+	left: 0;
+}
+
+/* #ifndef APP-NVUE */
+.zp-scroll-view-hide-scrollbar ::-webkit-scrollbar {
+	display: none;
+	-webkit-appearance: none;
+	width: 0 !important;
+	height: 0 !important;
+	background: transparent;
+}
+/* #endif */
+
+.zp-paging-touch-view {
+	width: 100%;
+	height: 100%;
+	position: relative;
+}
+
+.zp-fixed-bac-view {
+	position: absolute;
+	width: 100%;
+	top: 0;
+	left: 0;
+	height: 200px;
+}
+
+.zp-paging-main {
+	height: 100%;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
+}
+
+.zp-paging-container {
+	flex: 1;
+	position: relative;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
+}
+
+.zp-chat-record-loading-custom-image {
+	width: 35rpx;
+	height: 35rpx;
+	/* #ifndef APP-NVUE */
+	animation: loading-flower 1s linear infinite;
+	/* #endif */
+}
+
+.zp-page-bottom-keyboard-placeholder-animate {
+	transition-property: height;
+	transition-duration: 0.15s;
+	/* #ifndef APP-NVUE */
+	will-change: height;
+	/* #endif */
+}
+
+.zp-custom-refresher-container {
+	overflow: hidden;
+}
+
+.zp-custom-refresher-refresh {
+	/* #ifndef APP-NVUE */
+	display: block;
+	/* #endif */
+}
+
+.zp-back-to-top {
+	z-index: 999;
+	position: absolute;
+	bottom: 0rpx;
+	transition-duration: .3s;
+	transition-property: opacity;
+}
+.zp-back-to-top-rpx {
+	width: 76rpx;
+	height: 76rpx;
+	bottom: 0rpx;
+	right: 25rpx;
+}
+.zp-back-to-top-px {
+	width: 38px;
+	height: 38px;
+	bottom: 0px;
+	right: 13px;
+}
+
+.zp-back-to-top-show {
+	opacity: 1;
+}
+
+.zp-back-to-top-hide {
+	opacity: 0;
+}
+
+.zp-back-to-top-img {
+	/* #ifndef APP-NVUE */
+	width: 100%;
+	height: 100%;
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	flex: 1;
+	/* #endif */
+	z-index: 999;
+}
+
+.zp-back-to-top-img-inversion {
+	transform: rotate(180deg);
+}
+
+.zp-empty-view {
+	/* #ifdef APP-NVUE */
+	height: 100%;
+	/* #endif */
+	flex: 1;
+}
+
+.zp-empty-view-center {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+}
+
+.zp-loading-fixed {
+	z-index: 9999;
+}
+
+.zp-safe-area-inset-bottom {
+	position: absolute;
+	/* #ifndef APP-PLUS */
+	height: env(safe-area-inset-bottom);
+	/* #endif */
+}
+
+.zp-n-refresh-container {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	justify-content: center;
+	width: 750rpx;
+}
+
+.zp-n-list-container{
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+	flex: 1;
+}

+ 50 - 0
uni_modules/z-paging/components/z-paging/css/z-paging-static.css

@@ -0,0 +1,50 @@
+/* [z-paging]公用的静态css资源 */
+
+.zp-line-loading-image {
+	/* #ifndef APP-NVUE */
+	animation: loading-flower 1s steps(12) infinite;
+	/* #endif */
+	color: #666666;
+}
+.zp-line-loading-image-rpx {
+	margin-right: 8rpx;
+	width: 34rpx;
+	height: 34rpx;
+}
+.zp-line-loading-image-px {
+	margin-right: 4px;
+	width: 17px;
+	height: 17px;
+}
+
+.zp-loading-image-ios-rpx {
+	width: 40rpx;
+	height: 40rpx;
+}
+.zp-loading-image-ios-px {
+	width: 20px;
+	height: 20px;
+}
+
+.zp-loading-image-android-rpx {
+	width: 34rpx;
+	height: 34rpx;
+}
+.zp-loading-image-android-px {
+	width: 17px;
+	height: 17px;
+}
+
+/* #ifndef APP-NVUE */
+@keyframes loading-flower {
+	0% {
+		-webkit-transform: rotate(0deg);
+		transform: rotate(0deg);
+	}
+	to {
+		-webkit-transform: rotate(1turn);
+		transform: rotate(1turn);
+	}
+}
+/* #endif */
+

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

@@ -0,0 +1,23 @@
+{	
+	"zp.refresher.default": "Pull down to refresh",
+	"zp.refresher.pulling": "Release to refresh",
+	"zp.refresher.refreshing": "Refreshing...",
+	"zp.refresher.complete": "Refresh succeeded",
+	"zp.refresher.f2": "Refresh to enter 2f",
+	
+	"zp.loadingMore.default": "Click to load more",
+	"zp.loadingMore.loading": "Loading...",
+	"zp.loadingMore.noMore": "No more data",
+	"zp.loadingMore.fail": "Load failed,click to reload",
+	
+	"zp.emptyView.title": "No data",
+	"zp.emptyView.reload": "Reload",
+	"zp.emptyView.error": "Sorry,load failed",
+	
+	"zp.refresherUpdateTime.title": "Last update: ",
+	"zp.refresherUpdateTime.none": "None",
+	"zp.refresherUpdateTime.today": "Today",
+	"zp.refresherUpdateTime.yesterday": "Yesterday",
+	
+	"zp.systemLoading.title": "Loading..."
+}

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

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

@@ -0,0 +1,23 @@
+{	
+	"zp.refresher.default": "继续下拉刷新",
+	"zp.refresher.pulling": "松开立即刷新",
+	"zp.refresher.refreshing": "正在刷新...",
+	"zp.refresher.complete": "刷新成功",
+	"zp.refresher.f2": "松手进入二楼",
+	
+	"zp.loadingMore.default": "点击加载更多",
+	"zp.loadingMore.loading": "正在加载...",
+	"zp.loadingMore.noMore": "没有更多了",
+	"zp.loadingMore.fail": "加载失败,点击重新加载",
+	
+	"zp.emptyView.title": "没有数据哦~",
+	"zp.emptyView.reload": "重新加载",
+	"zp.emptyView.error": "很抱歉,加载失败",
+	
+	"zp.refresherUpdateTime.title": "最后更新:",
+	"zp.refresherUpdateTime.none": "无",
+	"zp.refresherUpdateTime.today": "今天",
+	"zp.refresherUpdateTime.yesterday": "昨天",
+	
+	"zp.systemLoading.title": "加载中..."
+}

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

@@ -0,0 +1,23 @@
+{	
+	"zp.refresher.default": "繼續下拉重繪",
+	"zp.refresher.pulling": "鬆開立即重繪",
+	"zp.refresher.refreshing": "正在重繪...",
+	"zp.refresher.complete": "重繪成功",
+	"zp.refresher.f2": "鬆手進入二樓",
+	
+	"zp.loadingMore.default": "點擊加載更多",
+	"zp.loadingMore.loading": "正在加載...",
+	"zp.loadingMore.noMore": "沒有更多了",
+	"zp.loadingMore.fail": "加載失敗,點擊重新加載",
+	
+	"zp.emptyView.title": "沒有數據哦~",
+	"zp.emptyView.reload": "重新加載",
+	"zp.emptyView.error": "很抱歉,加載失敗",
+	
+	"zp.refresherUpdateTime.title": "最後更新:",
+	"zp.refresherUpdateTime.none": "無",
+	"zp.refresherUpdateTime.today": "今天",
+	"zp.refresherUpdateTime.yesterday": "昨天",
+	
+	"zp.systemLoading.title": "加載中..."
+}

+ 25 - 0
uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js

@@ -0,0 +1,25 @@
+// [z-paging]useZPaging hooks
+
+import { onPageScroll, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
+
+function useZPaging(paging) {
+	const cPaging = !!paging ? paging.value || paging : null;
+	
+	onPullDownRefresh(() => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.reload().catch(() => {});
+	})
+	
+	onPageScroll(e => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.updatePageScrollTop(e.scrollTop);
+		e.scrollTop < 10 && cPaging.value.doChatRecordLoadMore();
+	})
+	
+	onReachBottom(() => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.pageReachBottom();
+	})
+}
+
+export default useZPaging

+ 25 - 0
uni_modules/z-paging/components/z-paging/js/hooks/useZPagingComp.js

@@ -0,0 +1,25 @@
+// [z-paging]useZPagingComp hooks
+
+function useZPagingComp(paging) {
+	const cPaging = !!paging ? paging.value || paging : null;
+	
+	const reload = () => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.reload().catch(() => {});
+	}
+	const updatePageScrollTop = scrollTop => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.updatePageScrollTop(scrollTop);
+	}
+	const doChatRecordLoadMore = () => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.doChatRecordLoadMore();
+	}
+	const pageReachBottom = () => {
+		if (!cPaging || !cPaging.value) return;
+		cPaging.value.pageReachBottom();
+	}
+	return { reload, updatePageScrollTop, doChatRecordLoadMore, pageReachBottom };
+}
+
+export default useZPagingComp

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

@@ -0,0 +1,125 @@
+// [z-paging]点击返回顶部view模块
+import u from '.././z-paging-utils'
+
+export default {
+	props: {
+		// 自动显示点击返回顶部按钮,默认为否
+		autoShowBackToTop: {
+			type: Boolean,
+			default: u.gc('autoShowBackToTop', false)
+		},
+		// 点击返回顶部按钮显示/隐藏的阈值(滚动距离),单位为px,默认为400rpx
+		backToTopThreshold: {
+			type: [Number, String],
+			default: u.gc('backToTopThreshold', '400rpx')
+		},
+		// 点击返回顶部按钮的自定义图片地址,默认使用z-paging内置的图片
+		backToTopImg: {
+			type: String,
+			default: u.gc('backToTopImg', '')
+		},
+		// 点击返回顶部按钮返回到顶部时是否展示过渡动画,默认为是
+		backToTopWithAnimate: {
+			type: Boolean,
+			default: u.gc('backToTopWithAnimate', true)
+		},
+		// 点击返回顶部按钮与底部的距离,注意添加单位px或rpx,默认为160rpx
+		backToTopBottom: {
+			type: [Number, String],
+			default: u.gc('backToTopBottom', '160rpx')
+		},
+		// 点击返回顶部按钮的自定义样式
+		backToTopStyle: {
+			type: Object,
+			default: u.gc('backToTopStyle', {}),
+		},
+		// iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向,默认为是
+		enableBackToTop: {
+			type: Boolean,
+			default: u.gc('enableBackToTop', true)
+		},
+	},
+	data() {
+		return {
+			// 点击返回顶部的class
+			backToTopClass: 'zp-back-to-top zp-back-to-top-hide',
+			// 上次点击返回顶部的时间
+			lastBackToTopShowTime: 0,
+			// 点击返回顶部显示的class是否在展示中,使得按钮展示/隐藏过度效果更自然
+			showBackToTopClass: false,
+		}
+	},
+	computed: {
+		backToTopThresholdUnitConverted() {
+			return u.addUnit(this.backToTopThreshold, this.unit);
+		},
+		backToTopBottomUnitConverted() {
+			return u.addUnit(this.backToTopBottom, this.unit);
+		},
+		finalEnableBackToTop() {
+			return this.usePageScroll ? false : this.enableBackToTop;
+		},
+		finalBackToTopThreshold() {
+			return u.convertToPx(this.backToTopThresholdUnitConverted);
+		},
+		finalBackToTopStyle() {
+			const backToTopStyle = this.backToTopStyle;
+			if (!backToTopStyle.bottom) {
+				backToTopStyle.bottom = this.windowBottom + u.convertToPx(this.backToTopBottomUnitConverted) + 'px';
+			}
+			if(!backToTopStyle.position){
+				backToTopStyle.position = this.usePageScroll ? 'fixed': 'absolute';
+			}
+			return backToTopStyle;
+		},
+		finalBackToTopClass() {
+			return `${this.backToTopClass} zp-back-to-top-${this.unit}`;
+		}
+	},
+	methods: {
+		// 点击了返回顶部
+		_backToTopClick() {
+			let callbacked = false;
+			this.$emit('backToTopClick', toTop => {
+				(toTop === undefined || toTop === true) && this._handleToTop();
+				callbacked = true;
+			});
+			// 如果用户没有禁止默认的返回顶部事件,则触发滚动到顶部
+			this.$nextTick(() => {
+				!callbacked && this._handleToTop();
+			})
+		},
+		// 处理滚动到顶部(聊天记录模式中为滚动到底部)
+		_handleToTop() {
+			!this.backToTopWithAnimate && this._checkShouldShowBackToTop(0);
+			!this.useChatRecordMode ? this.scrollToTop(this.backToTopWithAnimate) : this.scrollToBottom(this.backToTopWithAnimate);
+		},
+		// 判断是否要显示返回顶部按钮
+		_checkShouldShowBackToTop(scrollTop) {
+			if (!this.autoShowBackToTop) {
+				this.showBackToTopClass = false;
+				return;
+			}
+			if (scrollTop > this.finalBackToTopThreshold) {
+				if (!this.showBackToTopClass) {
+					// 记录当前点击返回顶部按钮显示的class生效了
+					this.showBackToTopClass = true;
+					this.lastBackToTopShowTime = new Date().getTime();
+					// 当滚动到需要展示返回顶部的阈值内,则延迟300毫秒展示返回到顶部按钮
+					u.delay(() => {
+						this.backToTopClass = 'zp-back-to-top zp-back-to-top-show';
+					}, 300)
+				}
+			} else {
+				// 如果当前点击返回顶部按钮显示的class是生效状态并且滚动小于触发阈值,则隐藏返回顶部按钮
+				if (this.showBackToTopClass) {
+					this.backToTopClass = 'zp-back-to-top zp-back-to-top-hide';
+					u.delay(() => {
+						this.showBackToTopClass = false;
+					}, new Date().getTime() - this.lastBackToTopShowTime < 500 ? 0 : 300)
+				}
+			}
+		},
+	}
+}
+

+ 153 - 0
uni_modules/z-paging/components/z-paging/js/modules/chat-record-mode.js

@@ -0,0 +1,153 @@
+// [z-paging]聊天记录模式模块
+import u from '.././z-paging-utils'
+
+export default {
+	props: {
+		// 使用聊天记录模式,默认为否
+		useChatRecordMode: {
+			type: Boolean,
+			default: u.gc('useChatRecordMode', false)
+		},
+		// 使用聊天记录模式时滚动到顶部后,列表垂直移动偏移距离。默认0rpx。单位px(暂时无效)
+		chatRecordMoreOffset: {
+			type: [Number, String],
+			default: u.gc('chatRecordMoreOffset', '0rpx')
+		},
+		// 使用聊天记录模式时是否自动隐藏键盘:在用户触摸列表时候自动隐藏键盘,默认为是
+		autoHideKeyboardWhenChat: {
+			type: Boolean,
+			default: u.gc('autoHideKeyboardWhenChat', true)
+		},
+		// 使用聊天记录模式中键盘弹出时是否自动调整slot="bottom"高度,默认为是
+		autoAdjustPositionWhenChat: {
+			type: Boolean,
+			default: u.gc('autoAdjustPositionWhenChat', true)
+		},
+		// 使用聊天记录模式中键盘弹出时占位高度偏移距离。默认0rpx。单位px
+		chatAdjustPositionOffset: {
+			type: [Number, String],
+			default: u.gc('chatAdjustPositionOffset', '0rpx')
+		},
+		// 使用聊天记录模式中键盘弹出时是否自动滚动到底部,默认为否
+		autoToBottomWhenChat: {
+			type: Boolean,
+			default: u.gc('autoToBottomWhenChat', false)
+		},
+		// 使用聊天记录模式中reload时是否显示chatLoading,默认为否
+		showChatLoadingWhenReload: {
+			type: Boolean,
+			default: u.gc('showChatLoadingWhenReload', false)
+		},
+		// 在聊天记录模式中滑动到顶部状态为默认状态时,以加载中的状态展示,默认为是。若设置为否,则默认会显示【点击加载更多】,然后才会显示loading
+		chatLoadingMoreDefaultAsLoading: {
+			type: Boolean,
+			default: u.gc('chatLoadingMoreDefaultAsLoading', true)
+		},
+	},
+	data() {
+		return {
+			// 键盘高度
+			keyboardHeight: 0,
+			// 键盘高度是否未改变,此时占位高度变化不需要动画效果
+			isKeyboardHeightChanged: false,
+		}
+	},
+	computed: {
+		finalChatRecordMoreOffset() {
+			return u.convertToPx(this.chatRecordMoreOffset);
+		},
+		finalChatAdjustPositionOffset() {
+			return u.convertToPx(this.chatAdjustPositionOffset);
+		},
+		// 聊天记录模式旋转180度style
+		chatRecordRotateStyle() {
+			let cellStyle;
+			// 在vue中,直接将列表倒置,因此在vue的cell中,也直接写style="transform: scaleY(-1)"转回来即可。
+			// #ifndef APP-NVUE
+			cellStyle = this.useChatRecordMode ? { transform: 'scaleY(-1)' } : {};
+			// #endif
+			
+			// 在nvue中,需要考虑数据量不满一页的情况,因为nvue中的list无法通过flex-end修改不满一页的起始位置,会导致不满一页时列表数据从底部开始,因此需要特别判断
+			// 当数据不满一屏的时候,不进行列表倒置
+			// #ifdef APP-NVUE
+			cellStyle = this.useChatRecordMode ? { transform: this.isFirstPageAndNoMore ? 'scaleY(1)' : 'scaleY(-1)' } : {};
+			// #endif
+			
+			this.$emit('update:cellStyle', cellStyle);
+			this.$emit('cellStyleChange', cellStyle);
+			
+			// 在聊天记录模式中,如果列表没有倒置并且当前是第一页,则需要自动滚动到最底部
+			this.$nextTick(() => {
+				if (this.isFirstPage && this.isChatRecordModeAndNotInversion) {
+					this.$nextTick(() => {
+						// 这里多次触发滚动到底部是为了避免在某些情况下,即使是在nextTick但是cell未渲染完毕导致滚动到底部位置不正确的问题
+						this._scrollToBottom(false);
+						u.delay(() => {
+							this._scrollToBottom(false);
+							u.delay(() => {
+								this._scrollToBottom(false);
+							}, 50)
+						}, 50)
+					})
+				}
+			})
+			return cellStyle;
+		},
+		// 是否是聊天记录列表并且有配置transform
+		isChatRecordModeHasTransform() {
+			return this.useChatRecordMode && this.chatRecordRotateStyle && this.chatRecordRotateStyle.transform;
+		},
+		// 是否是聊天记录列表并且列表未倒置
+		isChatRecordModeAndNotInversion() {
+			return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(1)';
+		},
+		// 是否是聊天记录列表并且列表倒置
+		isChatRecordModeAndInversion() {
+			return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(-1)';
+		},
+		// 最终的聊天记录模式中底部安全区域的高度,如果开启了底部安全区域并且键盘未弹出,则添加底部区域高度
+		chatRecordModeSafeAreaBottom() {
+			return this.safeAreaInsetBottom && !this.keyboardHeight ? this.safeAreaBottom : 0;
+		}
+	},
+	mounted() {
+		this.addKeyboardHeightChangeListener();
+	},
+	methods: {
+		// 添加聊天记录
+		addChatRecordData(data, toBottom = true, toBottomWithAnimate = true) {
+			if (!this.useChatRecordMode) return;
+			this.isTotalChangeFromAddData = true;
+			this.addDataFromTop(data, toBottom, toBottomWithAnimate);
+		},
+		// 手动触发滚动到顶部加载更多,聊天记录模式时有效
+		doChatRecordLoadMore() {
+			this.useChatRecordMode && this._onLoadingMore('click');
+		},
+		// 手动添加键盘高度变化监听
+		addKeyboardHeightChangeListener() {
+			// 监听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
+			// #ifndef H5 || MP-BAIDU || MP-TOUTIAO
+			if (this.useChatRecordMode) {
+				uni.onKeyboardHeightChange(this._handleKeyboardHeightChange);
+			}
+			// #endif
+		},
+		// 处理键盘高度变化
+		_handleKeyboardHeightChange(res) {
+			this.$emit('keyboardHeightChange', res);
+			if (this.autoAdjustPositionWhenChat) {
+				this.isKeyboardHeightChanged = true;
+				this.keyboardHeight = res.height > 0 ? res.height + this.finalChatAdjustPositionOffset : res.height;
+			}
+			if (this.autoToBottomWhenChat && this.keyboardHeight > 0) {
+				u.delay(() => {
+					this.scrollToBottom(false);
+					u.delay(() => {
+						this.scrollToBottom(false);
+					})
+				})
+			} 
+		}
+	}
+}

+ 152 - 0
uni_modules/z-paging/components/z-paging/js/modules/common-layout.js

@@ -0,0 +1,152 @@
+// [z-paging]通用布局相关模块
+import u from '.././z-paging-utils'
+
+// #ifdef APP-NVUE
+const weexDom = weex.requireModule('dom');
+// #endif
+
+export default {
+	data() {
+		return {
+			systemInfo: null,
+			cssSafeAreaInsetBottom: -1,
+			isReadyDestroy: false,
+		}
+	},
+	computed: {
+		// 顶部可用距离
+		windowTop() {
+			if (!this.systemInfo) return 0;
+			// 暂时修复vue3中隐藏系统导航栏后windowTop获取不正确的问题,具体bug详见https://ask.dcloud.net.cn/question/141634
+			// 感谢litangyu!!https://github.com/SmileZXLee/uni-z-paging/issues/25
+			// #ifdef VUE3 && H5
+			const pageHeadNode = document.getElementsByTagName("uni-page-head");
+			if (!pageHeadNode.length) return 0;
+			// #endif
+			return this.systemInfo.windowTop || 0;
+		},
+		// 底部安全区域高度
+		safeAreaBottom() {
+			if (!this.systemInfo) return 0;
+			let safeAreaBottom = 0;
+			// #ifdef APP-PLUS
+			safeAreaBottom = this.systemInfo.safeAreaInsets.bottom || 0 ;
+			// #endif
+			// #ifndef APP-PLUS
+			safeAreaBottom = Math.max(this.cssSafeAreaInsetBottom, 0);
+			// #endif
+			return safeAreaBottom;
+		},
+		// 是否是比较老的webview,在一些老的webview中,需要进行一些特殊处理
+		isOldWebView() {
+			// #ifndef APP-NVUE || MP-KUAISHOU
+			try {
+				const systemInfos = u.getSystemInfoSync(true).system.split(' ');
+				const deviceType = systemInfos[0];
+				const version = parseInt(systemInfos[1]);
+				if ((deviceType === 'iOS' && version <= 10) || (deviceType === 'Android' && version <= 6)) {
+					return true;
+				}
+			} catch(e) {
+				return false;
+			}
+			// #endif
+			return false;
+		},
+		// 当前组件的$slots,兼容不同平台
+		zSlots() {
+			// #ifdef VUE2
+			
+			// #ifdef MP-ALIPAY
+			return this.$slots;
+			// #endif
+			
+			return this.$scopedSlots || this.$slots;
+			// #endif
+			
+			return this.$slots;
+		},
+	},
+	beforeDestroy() {
+		this.isReadyDestroy = true;
+	},
+	// #ifdef VUE3
+	unmounted() {
+		this.isReadyDestroy = true;
+	},
+	// #endif
+	methods: {
+		// 更新fixed模式下z-paging的布局
+		updateFixedLayout() {
+			this.fixed && this.$nextTick(() => {
+				this.systemInfo = u.getSystemInfoSync();
+			})
+		},
+		// 获取节点尺寸
+		_getNodeClientRect(select, inDom = true, scrollOffset = false) {
+			if (this.isReadyDestroy) {
+				return Promise.resolve(false);
+			};
+			// nvue中获取节点信息
+			// #ifdef APP-NVUE
+			select = select.replace(/[.|#]/g, '');
+			const ref = this.$refs[select];
+			return new Promise((resolve, reject) => {
+				if (ref) {
+					weexDom.getComponentRect(ref, option => {
+						resolve(option && option.result ? [option.size] : false);
+					})
+				} else {
+					resolve(false);
+				}
+			});
+			return;
+			// #endif
+			
+			// vue中获取节点信息
+			//#ifdef MP-ALIPAY
+			inDom = false;
+			//#endif
+			
+			/*
+			inDom可能是true、false,也可能是具体的dom节点
+			如果inDom不为false,则使用uni.createSelectorQuery().in()进行查询,如果inDom为true,则in中的是this,否则in中的为具体的dom
+			如果inDom为false,则使用uni.createSelectorQuery()进行查询
+			*/
+			let res = !!inDom ? uni.createSelectorQuery().in(inDom === true ? this : inDom) : uni.createSelectorQuery();
+			scrollOffset ? res.select(select).scrollOffset() : res.select(select).boundingClientRect();
+			return new Promise((resolve, reject) => {
+				res.exec(data => {
+					resolve((data && data != '' && data != undefined && data.length) ? data : false);
+				});
+			});
+		},
+		// 获取slot="left"和slot="right"宽度并且更新布局
+		_updateLeftAndRightWidth(targetStyle, parentNodePrefix) {
+			this.$nextTick(() => {
+				let delayTime = 0;
+				// #ifdef MP-BAIDU
+				delayTime = 10;
+				// #endif
+				setTimeout(() => {
+					['left','right'].map(position => {
+						this._getNodeClientRect(`.${parentNodePrefix}-${position}`).then(res => {
+							this.$set(targetStyle, position, res ? res[0].width + 'px' : '0px');
+						});
+					})
+				}, delayTime)
+			})
+		},
+		// 通过获取css设置的底部安全区域占位view高度设置bottom距离(直接通过systemInfo在部分平台上无法获取到底部安全区域)
+		_getCssSafeAreaInsetBottom(success) {
+			this._getNodeClientRect('.zp-safe-area-inset-bottom').then(res => {
+				this.cssSafeAreaInsetBottom = res ? res[0].height : -1;
+				res && success && success();
+			});
+		},
+		// 同步获取系统信息,兼容不同平台(供z-paging-swiper使用)
+		_getSystemInfoSync(useCache = false) {
+			return u.getSystemInfoSync(useCache);
+		}
+	}
+}

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

@@ -0,0 +1,744 @@
+// [z-paging]数据处理模块
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import Enum from '.././z-paging-enum'
+import interceptor from '../z-paging-interceptor'
+
+export default {
+	props: {
+		// 自定义初始的pageNo,默认为1
+		defaultPageNo: {
+			type: Number,
+			default: u.gc('defaultPageNo', 1),
+			observer: function(newVal) {
+				this.pageNo = newVal;
+			},
+		},
+		// 自定义pageSize,默认为10
+		defaultPageSize: {
+			type: Number,
+			default: u.gc('defaultPageSize', 10),
+			validator: (value) => {
+				if (value <= 0) u.consoleErr('default-page-size必须大于0!');
+				return value > 0;
+			}
+		},
+		// 为保证数据一致,设置当前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
+		dataKey: {
+			type: [Number, String, Object],
+			default: u.gc('dataKey', null),
+		},
+		// 使用缓存,若开启将自动缓存第一页的数据,默认为否。请注意,因考虑到切换tab时不同tab数据不同的情况,默认仅会缓存组件首次加载时第一次请求到的数据,后续的下拉刷新操作不会更新缓存。
+		useCache: {
+			type: Boolean,
+			default: u.gc('useCache', false)
+		},
+		// 使用缓存时缓存的key,用于区分不同列表的缓存数据,useCache为true时必须设置,否则缓存无效
+		cacheKey: {
+			type: String,
+			default: u.gc('cacheKey', null)
+		},
+		// 缓存模式,默认仅会缓存组件首次加载时第一次请求到的数据,可设置为always,即代表总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
+		cacheMode: {
+			type: String,
+			default: u.gc('cacheMode', Enum.CacheMode.Default)
+		},
+		// 自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
+		autowireListName: {
+			type: String,
+			default: u.gc('autowireListName', '')
+		},
+		// 自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
+		autowireQueryName: {
+			type: String,
+			default: u.gc('autowireQueryName', '')
+		},
+		// 获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
+		fetch: {
+			type: Function,
+			default: null
+		},
+		// fetch的附加参数,fetch配置后有效
+		fetchParams: {
+			type: Object,
+			default: u.gc('fetchParams', null)
+		},
+		// z-paging mounted后自动调用reload方法(mounted后自动调用接口),默认为是
+		auto: {
+			type: Boolean,
+			default: u.gc('auto', true)
+		},
+		// 用户下拉刷新时是否触发reload方法,默认为是
+		reloadWhenRefresh: {
+			type: Boolean,
+			default: u.gc('reloadWhenRefresh', true)
+		},
+		// reload时自动滚动到顶部,默认为是
+		autoScrollToTopWhenReload: {
+			type: Boolean,
+			default: u.gc('autoScrollToTopWhenReload', true)
+		},
+		// reload时立即自动清空原list,默认为是,若立即自动清空,则在reload之后、请求回调之前页面是空白的
+		autoCleanListWhenReload: {
+			type: Boolean,
+			default: u.gc('autoCleanListWhenReload', true)
+		},
+		// 列表刷新时自动显示下拉刷新view,默认为否
+		showRefresherWhenReload: {
+			type: Boolean,
+			default: u.gc('showRefresherWhenReload', false)
+		},
+		// 列表刷新时自动显示加载更多view,且为加载中状态,默认为否
+		showLoadingMoreWhenReload: {
+			type: Boolean,
+			default: u.gc('showLoadingMoreWhenReload', false)
+		},
+		// 组件created时立即触发reload(可解决一些情况下先看到页面再看到loading的问题),auto为true时有效。为否时将在mounted+nextTick后触发reload,默认为否
+		createdReload: {
+			type: Boolean,
+			default: u.gc('createdReload', false)
+		},
+		// 本地分页时上拉加载更多延迟时间,单位为毫秒,默认200毫秒
+		localPagingLoadingTime: {
+			type: [Number, String],
+			default: u.gc('localPagingLoadingTime', 200)
+		},
+		// 自动拼接complete中传过来的数组(使用聊天记录模式时无效)
+		concat: {
+			type: Boolean,
+			default: u.gc('concat', true)
+		},
+		// 请求失败是否触发reject,默认为是
+		callNetworkReject: {
+			type: Boolean,
+			default: u.gc('callNetworkReject', true)
+		},
+		// 父组件v-model所绑定的list的值
+		value: {
+			type: Array,
+			default: function() {
+				return [];
+			}
+		},
+		// #ifdef VUE3
+		modelValue: {
+			type: Array,
+			default: function() {
+				return [];
+			}
+		}
+		// #endif
+	},
+	data (){
+		return {
+			currentData: [],
+			totalData: [],
+			realTotalData: [],
+			totalLocalPagingList: [],
+			dataPromiseResultMap: {
+				reload: null,
+				complete: null,
+				localPaging: null
+			},
+			isSettingCacheList: false,
+			pageNo: 1,
+			currentRefreshPageSize: 0,
+			isLocalPaging: false,
+			isAddedData: false,
+			isTotalChangeFromAddData: false,
+			privateConcat: true,
+			myParentQuery: -1,
+			firstPageLoaded: false,
+			pagingLoaded: false,
+			loaded: false,
+			isUserReload: true,
+			fromEmptyViewReload: false,
+			queryFrom: '',
+			listRendering: false,
+			isHandlingRefreshToPage: false,
+			isFirstPageAndNoMore: false,
+			totalDataChangeThrow: true,
+			addDataFromTopBufferedInsert: u.useBufferedInsert(this._addDataFromTop)
+		}
+	},
+	computed: {
+		pageSize() {
+			return this.defaultPageSize;
+		},
+		finalConcat() {
+			return this.concat && this.privateConcat;
+		},
+		finalUseCache() {
+			if (this.useCache && !this.cacheKey) {
+				u.consoleErr('use-cache为true时,必须设置cache-key,否则缓存无效!');
+			}
+			return this.useCache && !!this.cacheKey;
+		},
+		finalCacheKey() {
+			return this.cacheKey ? `${c.cachePrefixKey}-${this.cacheKey}` : null; 
+		},
+		isFirstPage() {
+			return this.pageNo === this.defaultPageNo;
+		}
+	},
+	watch: {
+		totalData(newVal, oldVal) {
+			this._totalDataChange(newVal, oldVal, this.totalDataChangeThrow);
+			this.totalDataChangeThrow = true;
+		},
+		currentData(newVal, oldVal) {
+			this._currentDataChange(newVal, oldVal);
+		},
+		useChatRecordMode(newVal, oldVal) {
+			if (newVal) {
+				this.nLoadingMoreFixedHeight = false;
+			}
+		},
+		value: {
+			handler(newVal) {
+				// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
+				if (newVal !== this.totalData) {
+					this.totalDataChangeThrow = false;
+					this.totalData = newVal;
+				}
+			},
+			immediate: true
+		},
+		// #ifdef VUE3
+		modelValue: {
+			handler(newVal) {
+				// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
+				if (newVal !== this.totalData) {
+					this.totalDataChangeThrow = false;
+					this.totalData = newVal;
+				}
+			},
+			immediate: true
+		}
+		// #endif
+	},
+	methods: {
+		// 请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否成功(默认为是)
+		complete(data, success = true) {
+			this.customNoMore = -1;
+			return this.addData(data, success);
+		},
+		//【保证数据一致】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为dataKey,需与:data-key绑定的一致,第三个参数为是否成功(默认为是)
+		completeByKey(data, dataKey = null, success = true) {
+			if (dataKey !== null && this.dataKey !== null && dataKey !== this.dataKey) {
+				this.isFirstPage && this.endRefresh();
+				return new Promise(resolve => resolve());
+			}
+			this.customNoMore = -1;
+			return this.addData(data, success);
+		},
+		//【通过total判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为total(列表总数),第三个参数为是否成功(默认为是)
+		completeByTotal(data, total, success = true) {
+			if (total == 'undefined') {
+				this.customNoMore = -1;
+			} else {
+				const dataTypeRes = this._checkDataType(data, success, false);
+				data = dataTypeRes.data;
+				success = dataTypeRes.success;
+				if (total >= 0 && success) {
+					return new Promise((resolve, reject) => {
+						this.$nextTick(() => {
+							let nomore = false;
+							const realTotalDataCount = this.pageNo == this.defaultPageNo ? 0 : this.realTotalData.length;
+							const dataLength = this.privateConcat ? data.length : 0;
+							let exceedCount = realTotalDataCount + dataLength - total;
+							// 没有更多数据了
+							if (exceedCount >= 0) {
+								nomore = true;
+								// 仅截取total内部分的数据
+								exceedCount = this.defaultPageSize - exceedCount;
+								if (this.privateConcat && exceedCount > 0 && exceedCount < data.length) {
+									data = data.splice(0, exceedCount);
+								}
+							}
+							this.completeByNoMore(data, nomore, success).then(res => resolve(res)).catch(() => reject());
+						})
+					});
+				}
+			}
+			return this.addData(data, success);
+		},
+		//【自行判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否没有更多数据,第三个参数为是否成功(默认是是)
+		completeByNoMore(data, nomore, success = true) {
+			if (nomore != 'undefined') {
+				this.customNoMore = nomore == true ? 1 : 0;
+			}
+			return this.addData(data, success);
+		},
+		// 请求结束且请求失败时调用,支持传入请求失败原因
+		completeByError(errorMsg) {
+			this.customerEmptyViewErrorText = errorMsg;
+			return this.complete(false);
+		},
+		// 与上方complete方法功能一致,新版本中设置服务端回调数组请使用complete方法
+		addData(data, success = true) {
+			if (!this.fromCompleteEmit) {
+				this.disabledCompleteEmit = true;
+				this.fromCompleteEmit = false;
+			}
+			const currentTimeStamp = u.getTime();
+			const disTime = currentTimeStamp - this.requestTimeStamp;
+			let minDelay = this.minDelay;
+			if (this.isFirstPage && this.finalShowRefresherWhenReload) {
+				minDelay = Math.max(400, minDelay);
+			}
+			const addDataDalay = (this.requestTimeStamp > 0 && disTime < minDelay) ? minDelay - disTime : 0;
+			this.$nextTick(() => {
+				u.delay(() => {
+					this._addData(data, success, false);
+				}, this.delay > 0 ? this.delay : addDataDalay)
+			})
+			
+			return new Promise((resolve, reject) => {
+				this.dataPromiseResultMap.complete = { resolve, reject };
+			});
+		},
+		// 从顶部添加数据,不会影响分页的pageNo和pageSize
+		addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
+			// 如果使用了虚拟列表,则需要对短时间内的大量数据进行整合然后一次性添加,避免设置虚拟列表cellIndex时候key冲突的问题,否则正常调用
+			(this.finalUseVirtualList ? this.addDataFromTopBufferedInsert : this._addDataFromTop)(
+				data, toTop, toTopWithAnimate
+			);
+		},
+		// 重新设置列表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
+		resetTotalData(data) {
+			this.isTotalChangeFromAddData = true;
+			data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : data;
+			this.totalData = data;
+		},
+		// 设置本地分页数据,请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging作分页处理(若调用了此方法,则上拉加载更多时内部会自动分页,不会触发@query所绑定的事件)
+		setLocalPaging(data, success = true) {
+			this.isLocalPaging = true;
+			this.$nextTick(() => {
+				this._addData(data, success, true);
+			})
+			return new Promise((resolve, reject) => {
+				this.dataPromiseResultMap.localPaging = { resolve, reject };
+			});
+		},
+		// 重新加载分页数据,pageNo会恢复为默认值,相当于下拉刷新的效果(animate为true时会展示下拉刷新动画,默认为false)
+		reload(animate = this.showRefresherWhenReload) {
+			if (animate) {
+				this.privateShowRefresherWhenReload = animate;
+				this.isUserPullDown = true;
+			}
+			if (!this.showLoadingMoreWhenReload) {
+				this.listRendering = true;
+			}
+			this.$nextTick(() => {
+				this._preReload(animate, false);
+			})
+			return new Promise((resolve, reject) => {
+				this.dataPromiseResultMap.reload = { resolve, reject };
+			});
+		},
+		// 刷新列表数据,pageNo和pageSize不会重置,列表数据会重新从服务端获取。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
+		refresh() {
+			return this._handleRefreshWithDisPageNo(this.pageNo - this.defaultPageNo + 1);
+		},
+		// 刷新列表数据至指定页,例如pageNo=5时则代表刷新列表至第5页,此时pageNo会变为5,列表会展示前5页的数据。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
+		refreshToPage(pageNo) {
+			this.isHandlingRefreshToPage = true;
+			return this._handleRefreshWithDisPageNo(pageNo + this.defaultPageNo - 1);
+		},
+		// 手动更新列表缓存数据,将自动截取v-model绑定的list中的前pageSize条覆盖缓存,请确保在list数据更新到预期结果后再调用此方法
+		updateCache() {
+			if (this.finalUseCache && this.totalData.length) {
+				this._saveLocalCache(this.totalData.slice(0, Math.min(this.totalData.length, this.pageSize)));
+			}
+		},
+		// 清空分页数据
+		clean() {
+			this._reload(true);
+			this._addData([], true, false);
+		},
+		// 清空分页数据
+		clear() {
+			this.clean();
+		},
+		// reload之前的一些处理
+		_preReload(animate = this.showRefresherWhenReload, isFromMounted = true, retryCount = 0) {
+			const showRefresher = this.finalRefresherEnabled && this.useCustomRefresher;
+			// #ifndef APP-NVUE
+			// 如果获取slot="refresher"高度失败,则不触发reload,直到获取slot="refresher"高度成功
+			if (this.customRefresherHeight === -1 && showRefresher) {
+				u.delay(() => {
+					retryCount ++;
+					// 如果重试次数是10的倍数(也就是每500毫秒),尝试重新获取一下slot="refresher"高度
+					// 此举是为了解决在某些特殊情况下,z-paging组件mounted了,但是未展示在用户面前,(比如在tabbar页面中,未切换到对应tabbar但是通过代码让z-paging展示了,此时控制台会报Error: Not Found:Page,因为这时候去获取dom节点信息获取不到)
+					// 当用户在某个时刻让此z-paging展示在面前时,即可顺利获取到slot="refresher"高度,递归停止
+					if (retryCount % 10 === 0) {
+						this._updateCustomRefresherHeight();
+					}
+					this._preReload(animate, isFromMounted, retryCount);
+				}, c.delayTime / 2);
+				return;
+			}
+			// #endif
+			this.isUserReload = true;
+			this.loadingType = Enum.LoadingType.Refresher;
+			if (animate) {
+				this.privateShowRefresherWhenReload = animate;
+				// #ifndef APP-NVUE
+				if (this.useCustomRefresher) {
+					this._doRefresherRefreshAnimate();
+				} else {
+					this.refresherTriggered = true;
+				}
+				// #endif
+				// #ifdef APP-NVUE
+				this.refresherStatus = Enum.Refresher.Loading;
+				this.refresherRevealStackCount ++;
+				u.delay(() => {
+					this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
+						if (node) {
+							let nodeHeight = node[0].height;
+							this.nShowRefresherReveal = true;
+							this.nShowRefresherRevealHeight = nodeHeight;
+							u.delay(() => {
+								this._nDoRefresherEndAnimation(0, -nodeHeight, false, false);
+								u.delay(() => {
+									this._nDoRefresherEndAnimation(nodeHeight, 0);
+								}, 10)
+							}, 10)
+						}
+						this._reload(false, isFromMounted);
+						this._doRefresherLoad(false);
+					});
+				}, this.pagingLoaded ? 10 : 100)
+				return;
+				// #endif
+			} else {
+				this._refresherEnd(false, false, false, false);
+			}
+			this._reload(false, isFromMounted);
+		},
+		// 重新加载分页数据
+		_reload(isClean = false, isFromMounted = false, isUserPullDown = false) {
+			this.isAddedData = false;
+			this.insideOfPaging = -1;
+			this.cacheScrollNodeHeight = -1;
+			this.pageNo = this.defaultPageNo;
+			this._cleanRefresherEndTimeout();
+			!this.privateShowRefresherWhenReload && !isClean && this._startLoading(true);
+			this.firstPageLoaded = true;
+			this.isTotalChangeFromAddData = false;
+			if (!this.isSettingCacheList) {
+				this.totalData = [];
+			}
+			if (!isClean) {
+				this._emitQuery(this.pageNo, this.defaultPageSize, isUserPullDown ? Enum.QueryFrom.UserPullDown : Enum.QueryFrom.Reload);
+				let delay = 0;
+				// #ifdef MP-TOUTIAO
+				delay = 5;
+				// #endif
+				u.delay(this._callMyParentQuery, delay);
+				if (!isFromMounted && this.autoScrollToTopWhenReload) {
+					let checkedNRefresherLoading = true;
+					// #ifdef APP-NVUE
+					checkedNRefresherLoading = !this.nRefresherLoading;
+					// #endif
+					checkedNRefresherLoading && this._scrollToTop(false);
+				}
+			}
+			// #ifdef APP-NVUE
+			this.$nextTick(() => {
+				this.nShowBottom = this.realTotalData.length > 0;
+			})
+			// #endif
+		},
+		// 处理服务端返回的数组
+		_addData(data, success, isLocal) {
+			this.isAddedData = true;
+			this.fromEmptyViewReload = false;
+			this.isTotalChangeFromAddData = true;
+			this.refresherTriggered = false;
+			this._endSystemLoadingAndRefresh();
+			const tempIsUserPullDown = this.isUserPullDown;
+			if (this.showRefresherUpdateTime && this.isFirstPage) {
+				u.setRefesrherTime(u.getTime(), this.refresherUpdateTimeKey);
+				this.$refs.refresh && this.$refs.refresh.updateTime();
+			}
+			if (!isLocal && tempIsUserPullDown && this.isFirstPage) {
+				this.isUserPullDown = false;
+			}
+			this.listRendering = true;
+			this.$nextTick(() => {
+				u.delay(() => this.listRendering = false);
+			})
+			let dataTypeRes = this._checkDataType(data, success, isLocal);
+			data = dataTypeRes.data;
+			success = dataTypeRes.success;
+			let delayTime = c.delayTime;
+			if (this.useChatRecordMode) delayTime = 0;
+			this.loadingForNow = false;
+			u.delay(() => {
+				this.pagingLoaded = true;
+				this.$nextTick(()=>{
+					!isLocal && this._refresherEnd(delayTime > 0, true, tempIsUserPullDown);
+				})
+			})
+			if (this.isFirstPage) {
+				this.isLoadFailed = !success;
+				this.$emit('isLoadFailedChange', this.isLoadFailed);
+				if (this.finalUseCache && success && (this.cacheMode === Enum.CacheMode.Always ? true : this.isSettingCacheList)) {
+					this._saveLocalCache(data);
+				}
+			}
+			this.isSettingCacheList = false;
+			if (success) {
+				if (!(this.privateConcat === false && !this.isHandlingRefreshToPage && this.loadingStatus === Enum.More.NoMore)) {
+					this.loadingStatus = Enum.More.Default;
+				}
+				if (isLocal) {
+					// 如果当前是本地分页,则必然是由setLocalPaging方法触发,此时直接本地加载第一页数据即可。后续本地分页加载更多方法由滚动到底部加载更多事件处理
+					this.totalLocalPagingList = data;
+					const localPageNo = this.defaultPageNo;
+					const localPageSize = this.queryFrom !== Enum.QueryFrom.Refresh ? this.defaultPageSize : this.currentRefreshPageSize;
+					this._localPagingQueryList(localPageNo, localPageSize, 0, res => {
+						u.delay(() => {
+							this.completeByTotal(res, this.totalLocalPagingList.length);;
+						}, 0)
+					})
+				} else {
+					// 如果当前不是本地分页,则按照正常分页逻辑进行数据处理&emit数据
+					let dataChangeDelayTime = 0;
+					// #ifdef APP-NVUE
+					if (this.privateShowRefresherWhenReload && this.finalNvueListIs === 'waterfall') {
+						dataChangeDelayTime = 150;
+					}
+					// #endif
+					u.delay(() => {
+						this._currentDataChange(data, this.currentData);
+						this._callDataPromise(true, this.totalData);
+					}, dataChangeDelayTime)
+				}
+				if (this.isHandlingRefreshToPage) {
+					this.isHandlingRefreshToPage = false;
+					this.pageNo = this.defaultPageNo + Math.ceil(data.length / this.pageSize) - 1;
+					if (data.length % this.pageSize !== 0) {
+						this.customNoMore = 1;
+					}
+				}
+			} else {
+				this._currentDataChange(data, this.currentData);
+				this._callDataPromise(false);
+				this.loadingStatus = Enum.More.Fail;
+				this.isHandlingRefreshToPage = false;
+				if (this.loadingType === Enum.LoadingType.LoadMore) {
+					this.pageNo --;
+				}
+			}
+		},
+		// 所有数据改变时调用
+		_totalDataChange(newVal, oldVal, eventThrow=true) {
+			if ((!this.isUserReload || !this.autoCleanListWhenReload) && this.firstPageLoaded && !newVal.length && oldVal.length) {
+				return;
+			}
+			this._doCheckScrollViewShouldFullHeight(newVal);
+			if(!this.realTotalData.length && !newVal.length){
+				eventThrow = false;
+			}
+			this.realTotalData = newVal;
+			// emit列表更新事件
+			if (eventThrow) {
+				this.$emit('input', newVal);
+				// #ifdef VUE3
+				this.$emit('update:modelValue', newVal);
+				// #endif
+				this.$emit('update:list', newVal);
+				this.$emit('listChange', newVal);
+				this._callMyParentList(newVal);
+			}
+			this.firstPageLoaded = false;
+			this.isTotalChangeFromAddData = false;
+			this.$nextTick(() => {
+				u.delay(()=>{
+					// emit z-paging内容区域高度改变事件
+					this._getNodeClientRect('.zp-paging-container-content').then(res => {
+						res && this.$emit('contentHeightChanged', res[0].height);
+					});
+				}, c.delayTime * (this.isIos ? 1 : 3))
+				// #ifdef APP-NVUE
+				// 在nvue中延时600毫秒展示底部加载更多,避免底部加载更多太早加载闪一下的问题
+				u.delay(() => {
+					this.nShowBottom = true;
+				}, c.delayTime * 6, 'nShowBottomDelay');
+				// #endif
+			})
+		},
+		// 当前数据改变时调用
+		_currentDataChange(newVal, oldVal) {
+			newVal = [...newVal];
+			// #ifndef APP-NVUE
+			this.finalUseVirtualList && this._setCellIndex(newVal, 'bottom');
+			// #endif
+			if (this.isFirstPage && this.finalConcat) {
+				this.totalData = [];
+			}
+			// customNoMore:-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
+			if (this.customNoMore !== -1) {
+				// 如果customNoMore等于1 或者 customNoMore不是0并且新增数组长度为0(也就是不是明确的还有更多数据并且新增的数组长度为0),则没有更多数据了
+				if (this.customNoMore === 1 || (this.customNoMore !== 0 && !newVal.length)) {
+					this.loadingStatus = Enum.More.NoMore;
+				}
+			} else {
+				// 如果新增的数据数组长度为0 或者 新增的数组长度小于默认的pageSize,则没有更多数据了
+				if (!newVal.length || (newVal.length && newVal.length < this.defaultPageSize)) {
+					this.loadingStatus = Enum.More.NoMore;
+				}
+			}
+			if (!this.totalData.length) {
+				// #ifdef APP-NVUE
+				// 如果在聊天记录模式+nvue中,并且数据不满一页时需要将列表倒序,因为此时没有将列表旋转180度,数组中第0条数据应当在最底下显示
+				if (this.useChatRecordMode && this.finalConcat && this.isFirstPage && this.loadingStatus === Enum.More.NoMore) {
+					newVal.reverse();
+				}
+				// #endif
+				this.totalData = newVal;
+			} else {
+				if (this.finalConcat) {
+					const currentScrollTop = this.oldScrollTop;
+					this.totalData = [...this.totalData, ...newVal];
+					// 此处是为了解决在微信小程序中,在某些情况下滚动到底部加载更多后滚动位置直接变为最底部的问题,因此需要通过代码强制滚动回加载更多前的位置
+					// #ifdef MP-WEIXIN
+					if (!this.isIos && !this.isOnly && !this.usePageScroll && newVal.length) {
+						this.loadingMoreTimeStamp = u.getTime();
+						this.$nextTick(() => {
+							this.scrollToY(currentScrollTop);
+						})
+					}
+					// #endif
+				} else {
+					this.totalData = newVal;
+				}
+			}
+			this.privateConcat = true;
+		},
+		// 根据pageNo处理refresh操作
+		_handleRefreshWithDisPageNo(pageNo) {
+			if (!this.isHandlingRefreshToPage && !this.realTotalData.length) return this.reload();
+			if (pageNo >= 1) {
+				this.loading = true;
+				this.privateConcat = false;
+				const totalPageSize = pageNo * this.pageSize;
+				this.currentRefreshPageSize = totalPageSize;
+				// 如果调用refresh时是本地分页,则在组件内部自己处理分页逻辑,不emit query相关事件
+				if (this.isLocalPaging && this.isHandlingRefreshToPage) {
+					this._localPagingQueryList(this.defaultPageNo, totalPageSize, 0, res => {
+						this.complete(res);
+					})
+				} else {
+					// emit query相关事件
+					this._emitQuery(this.defaultPageNo, totalPageSize, Enum.QueryFrom.Refresh);
+					this._callMyParentQuery(this.defaultPageNo, totalPageSize);
+				}
+			}
+			return new Promise((resolve, reject) => {
+				this.dataPromiseResultMap.reload = { resolve, reject };
+			});
+		},
+		// 本地分页请求
+		_localPagingQueryList(pageNo, pageSize, localPagingLoadingTime, callback) {
+			pageNo = Math.max(1, pageNo);
+			pageSize = Math.max(1, pageSize);
+			const totalPagingList = [...this.totalLocalPagingList];
+			const pageNoIndex = (pageNo - 1) * pageSize;
+			const finalPageNoIndex = Math.min(totalPagingList.length, pageNoIndex + pageSize);
+			const resultPagingList = totalPagingList.splice(pageNoIndex, finalPageNoIndex - pageNoIndex);
+			u.delay(() => callback(resultPagingList), localPagingLoadingTime)
+		},
+		// 从顶部添加数据,不会影响分页的pageNo和pageSize
+		_addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
+			// 数据是否拼接到顶部,如果是聊天记录模式并且列表没有倒置,则应该拼接在底部
+			let addFromTop = !this.isChatRecordModeAndNotInversion;
+			data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : (addFromTop ? data.reverse() : data);
+			// #ifndef APP-NVUE
+			this.finalUseVirtualList && this._setCellIndex(data, 'top')
+			// #endif
+			
+			this.totalData = addFromTop ? [...data, ...this.totalData] : [...this.totalData, ...data];
+			if (toTop) {
+				u.delay(() => this.useChatRecordMode ? this.scrollToBottom(toTopWithAnimate) : this.scrollToTop(toTopWithAnimate));
+			}
+		},
+		// 存储列表缓存数据
+		_saveLocalCache(data) {
+			uni.setStorageSync(this.finalCacheKey, data);
+		},
+		// 通过缓存数据填充列表数据
+		_setListByLocalCache() {
+			this.totalData = uni.getStorageSync(this.finalCacheKey) || [];
+			this.isSettingCacheList = true;
+		},
+		// 修改父view的list
+		_callMyParentList(newVal) {
+			if (this.autowireListName.length) {
+				const myParent = u.getParent(this.$parent);
+				if (myParent && myParent[this.autowireListName]) {
+					myParent[this.autowireListName] = newVal;
+				}
+			}
+		},
+		// 调用父view的query
+		_callMyParentQuery(customPageNo = 0, customPageSize = 0) {
+			if (this.autowireQueryName) {
+				if (this.myParentQuery === -1) {
+					const myParent = u.getParent(this.$parent);
+					if (myParent && myParent[this.autowireQueryName]) {
+						this.myParentQuery = myParent[this.autowireQueryName];
+					}
+				} 
+				if (this.myParentQuery !== -1) {
+					customPageSize > 0 ? this.myParentQuery(customPageNo, customPageSize) : this.myParentQuery(this.pageNo, this.defaultPageSize);
+				}
+			}
+		},
+		// emit query事件
+		_emitQuery(pageNo, pageSize, from){
+			this.queryFrom = from;
+			this.requestTimeStamp = u.getTime();
+			const [lastItem] = this.realTotalData.slice(-1);
+			if (this.fetch) {
+				const fetchParams = interceptor._handleFetchParams({pageNo, pageSize, from, lastItem: lastItem || null}, this.fetchParams);
+				const fetchResult = this.fetch(fetchParams);
+				if (!interceptor._handleFetchResult(fetchResult, this, fetchParams)) {
+					u.isPromise(fetchResult) ? fetchResult.then(res => {
+						this.complete(res);
+					}).catch(err => {
+						this.complete(false);
+					}) : this.complete(fetchResult)
+				}
+			} else {
+				this.$emit('query', ...interceptor._handleQuery(pageNo, pageSize, from, lastItem || null));
+			}
+		},
+		// 触发数据改变promise
+		_callDataPromise(success, totalList) {
+			for (const key in this.dataPromiseResultMap) {
+				const obj = this.dataPromiseResultMap[key];
+				if (!obj) continue;
+				success ? obj.resolve({ totalList, noMore: this.loadingStatus === Enum.More.NoMore }) : this.callNetworkReject && obj.reject(`z-paging-${key}-error`);
+			}
+		},
+		// 检查complete data的类型
+		_checkDataType(data, success, isLocal) {
+			const dataType = Object.prototype.toString.call(data);
+			if (dataType === '[object Boolean]') {
+				success = data;
+				data = [];
+			} else if (dataType !== '[object Array]') {
+				data = [];
+				if (dataType !== '[object Undefined]' && dataType !== '[object Null]') {
+					u.consoleErr(`${isLocal ? 'setLocalPaging' : 'complete'}参数类型不正确,第一个参数类型必须为Array!`);
+				}
+			}
+			return { data, success };
+		},
+	}
+}

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

@@ -0,0 +1,144 @@
+// [z-paging]空数据图view模块
+import u from '.././z-paging-utils'
+
+export default {
+	props: {
+		// 是否强制隐藏空数据图,默认为否
+		hideEmptyView: {
+			type: Boolean,
+			default: u.gc('hideEmptyView', false)
+		},
+		// 空数据图描述文字,默认为“没有数据哦~”
+		emptyViewText: {
+			type: [String, Object],
+			default: u.gc('emptyViewText', null)
+		},
+		// 是否显示空数据图重新加载按钮(无数据时),默认为否
+		showEmptyViewReload: {
+			type: Boolean,
+			default: u.gc('showEmptyViewReload', false)
+		},
+		// 加载失败时是否显示空数据图重新加载按钮,默认为是
+		showEmptyViewReloadWhenError: {
+			type: Boolean,
+			default: u.gc('showEmptyViewReloadWhenError', true)
+		},
+		// 空数据图点击重新加载文字,默认为“重新加载”
+		emptyViewReloadText: {
+			type: [String, Object],
+			default: u.gc('emptyViewReloadText', null)
+		},
+		// 空数据图图片,默认使用z-paging内置的图片
+		emptyViewImg: {
+			type: String,
+			default: u.gc('emptyViewImg', '')
+		},
+		// 空数据图“加载失败”描述文字,默认为“很抱歉,加载失败”
+		emptyViewErrorText: {
+			type: [String, Object],
+			default: u.gc('emptyViewErrorText', null)
+		},
+		// 空数据图“加载失败”图片,默认使用z-paging内置的图片
+		emptyViewErrorImg: {
+			type: String,
+			default: u.gc('emptyViewErrorImg', '')
+		},
+		// 空数据图样式
+		emptyViewStyle: {
+			type: Object,
+			default: u.gc('emptyViewStyle', {})
+		},
+		// 空数据图容器样式
+		emptyViewSuperStyle: {
+			type: Object,
+			default: u.gc('emptyViewSuperStyle', {})
+		},
+		// 空数据图img样式
+		emptyViewImgStyle: {
+			type: Object,
+			default: u.gc('emptyViewImgStyle', {})
+		},
+		// 空数据图描述文字样式
+		emptyViewTitleStyle: {
+			type: Object,
+			default: u.gc('emptyViewTitleStyle', {})
+		},
+		// 空数据图重新加载按钮样式
+		emptyViewReloadStyle: {
+			type: Object,
+			default: u.gc('emptyViewReloadStyle', {})
+		},
+		// 空数据图片是否铺满z-paging,默认为否,即填充满z-paging内列表(滚动区域)部分。若设置为否,则为填铺满整个z-paging
+		emptyViewFixed: {
+			type: Boolean,
+			default: u.gc('emptyViewFixed', false)
+		},
+		// 空数据图片是否垂直居中,默认为是,若设置为否即为从空数据容器顶部开始显示。emptyViewFixed为false时有效
+		emptyViewCenter: {
+			type: Boolean,
+			default: u.gc('emptyViewCenter', true)
+		},
+		// 加载中时是否自动隐藏空数据图,默认为是
+		autoHideEmptyViewWhenLoading: {
+			type: Boolean,
+			default: u.gc('autoHideEmptyViewWhenLoading', true)
+		},
+		// 用户下拉列表触发下拉刷新加载中时是否自动隐藏空数据图,默认为是
+		autoHideEmptyViewWhenPull: {
+			type: Boolean,
+			default: u.gc('autoHideEmptyViewWhenPull', true)
+		},
+		// 空数据view的z-index,默认为9
+		emptyViewZIndex: {
+			type: Number,
+			default: u.gc('emptyViewZIndex', 9)
+		},
+	},
+	data() {
+		return {
+			customerEmptyViewErrorText: ''
+		}
+	},
+	computed: {
+		finalEmptyViewImg() {
+			return this.isLoadFailed ? this.emptyViewErrorImg : this.emptyViewImg;
+		},
+		finalShowEmptyViewReload() {
+			return this.isLoadFailed ? this.showEmptyViewReloadWhenError : this.showEmptyViewReload;
+		},
+		// 是否展示空数据图
+		showEmpty() {
+			if (this.isOnly || this.hideEmptyView || this.realTotalData.length) return false;
+			if (this.autoHideEmptyViewWhenLoading) {
+				if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
+			} else {
+				return true;
+			}
+			return !this.autoHideEmptyViewWhenPull && !this.isUserReload;
+		},
+	},
+	methods: {
+		// 点击了空数据view重新加载按钮
+		_emptyViewReload() {
+			let callbacked = false;
+			this.$emit('emptyViewReload', reload => {
+				if (reload === undefined || reload === true) {
+					this.fromEmptyViewReload = true;
+					this.reload().catch(() => {});
+				}
+				callbacked = true;
+			});
+			// 如果用户没有禁止默认的点击重新加载刷新列表事件,则触发列表重新刷新
+			this.$nextTick(() => {
+				if (!callbacked) {
+					this.fromEmptyViewReload = true;
+					this.reload().catch(() => {});
+				}
+			})
+		},
+		// 点击了空数据view
+		_emptyViewClick() {
+			this.$emit('emptyViewClick');
+		},
+	}
+}

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

@@ -0,0 +1,113 @@
+// [z-paging]i18n模块
+import { initVueI18n } from '@dcloudio/uni-i18n'
+import messages from '../../i18n/index.js'
+const { t } = initVueI18n(messages)
+
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import interceptor from '../z-paging-interceptor'
+
+export default {
+	computed: {
+		finalLanguage() {
+			try {
+				const local = uni.getLocale();
+				const language = this.systemInfo.appLanguage;
+				return local === 'auto' ? interceptor._handleLanguage2Local(language, this._language2Local(language)) : local;
+			} catch (e) {
+				// 如果获取系统本地语言异常,则默认返回中文,uni.getLocale在部分低版本HX或者cli中可能报找不到的问题
+				return 'zh-Hans';
+			}
+		},
+		// 最终的下拉刷新默认状态的文字
+		finalRefresherDefaultText() {
+			return this._getI18nText('zp.refresher.default', this.refresherDefaultText);
+		},
+		// 最终的下拉刷新下拉中的文字
+		finalRefresherPullingText() {
+			return this._getI18nText('zp.refresher.pulling', this.refresherPullingText);
+		},
+		// 最终的下拉刷新中文字
+		finalRefresherRefreshingText() {
+			return this._getI18nText('zp.refresher.refreshing', this.refresherRefreshingText);
+		},
+		// 最终的下拉刷新完成文字
+		finalRefresherCompleteText() {
+			return this._getI18nText('zp.refresher.complete', this.refresherCompleteText);
+		},
+		// 最终的下拉刷新上次更新时间文字
+		finalRefresherUpdateTimeTextMap() {
+			return {
+				title: t('zp.refresherUpdateTime.title'),
+				none: t('zp.refresherUpdateTime.none'),
+				today: t('zp.refresherUpdateTime.today'),
+				yesterday: t('zp.refresherUpdateTime.yesterday')
+			};
+		},
+		// 最终的继续下拉进入二楼文字
+		finalRefresherGoF2Text() {
+			return this._getI18nText('zp.refresher.f2', this.refresherGoF2Text);
+		},
+		// 最终的底部加载更多默认状态文字
+		finalLoadingMoreDefaultText() {
+			return this._getI18nText('zp.loadingMore.default', this.loadingMoreDefaultText);
+		},
+		// 最终的底部加载更多加载中文字
+		finalLoadingMoreLoadingText() {
+			return this._getI18nText('zp.loadingMore.loading', this.loadingMoreLoadingText);
+		},
+		// 最终的底部加载更多没有更多数据文字
+		finalLoadingMoreNoMoreText() {
+			return this._getI18nText('zp.loadingMore.noMore', this.loadingMoreNoMoreText);
+		},
+		// 最终的底部加载更多加载失败文字
+		finalLoadingMoreFailText() {
+			return this._getI18nText('zp.loadingMore.fail', this.loadingMoreFailText);
+		},
+		// 最终的空数据图title
+		finalEmptyViewText() {
+			return this.isLoadFailed ? this.finalEmptyViewErrorText : this._getI18nText('zp.emptyView.title', this.emptyViewText);
+		},
+		// 最终的空数据图reload title
+		finalEmptyViewReloadText() {
+			return this._getI18nText('zp.emptyView.reload', this.emptyViewReloadText);
+		},
+		// 最终的空数据图加载失败文字
+		finalEmptyViewErrorText() {
+			return this.customerEmptyViewErrorText || this._getI18nText('zp.emptyView.error', this.emptyViewErrorText);
+		},
+		// 最终的系统loading title
+		finalSystemLoadingText() {
+			return this._getI18nText('zp.systemLoading.title', this.systemLoadingText);
+		},
+	},
+	methods: {
+		// 获取当前z-paging的语言
+		getLanguage() {
+			return this.finalLanguage;
+		},
+		// 获取国际化转换后的文本
+		_getI18nText(key, value) {
+			const dataType = Object.prototype.toString.call(value);
+			if (dataType === '[object Object]') {
+				const nextValue = value[this.finalLanguage];
+				if (nextValue) return nextValue;
+			} else if (dataType === '[object String]') {
+				return value;
+			}
+			return t(key);
+		},
+		// 系统language转i18n local
+		_language2Local(language) {
+			const formatedLanguage = language.toLowerCase().replace(new RegExp('_', ''), '-');
+			if (formatedLanguage.indexOf('zh') !== -1) {
+				if (formatedLanguage === 'zh' || formatedLanguage === 'zh-cn' || formatedLanguage.indexOf('zh-hans') !== -1) {
+					return 'zh-Hans';
+				}
+				return 'zh-Hant';
+			}
+			if (formatedLanguage.indexOf('en') !== -1) return 'en';
+			return language;
+		}
+	}
+}

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

@@ -0,0 +1,374 @@
+// [z-paging]滚动到底部加载更多模块
+import u from '.././z-paging-utils'
+import Enum from '.././z-paging-enum'
+
+export default {
+	props: {
+		// 自定义底部加载更多样式
+		loadingMoreCustomStyle: {
+			type: Object,
+			default: u.gc('loadingMoreCustomStyle', {})
+		},
+		// 自定义底部加载更多文字样式
+		loadingMoreTitleCustomStyle: {
+			type: Object,
+			default: u.gc('loadingMoreTitleCustomStyle', {})
+		},
+		// 自定义底部加载更多加载中动画样式
+		loadingMoreLoadingIconCustomStyle: {
+			type: Object,
+			default: u.gc('loadingMoreLoadingIconCustomStyle', {})
+		},
+		// 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
+		loadingMoreLoadingIconType: {
+			type: String,
+			default: u.gc('loadingMoreLoadingIconType', 'flower')
+		},
+		// 自定义底部加载更多加载中动画图标图片
+		loadingMoreLoadingIconCustomImage: {
+			type: String,
+			default: u.gc('loadingMoreLoadingIconCustomImage', '')
+		},
+		// 底部加载更多加载中view是否展示旋转动画,默认为是
+		loadingMoreLoadingAnimated: {
+			type: Boolean,
+			default: u.gc('loadingMoreLoadingAnimated', true)
+		},
+		// 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为是
+		loadingMoreEnabled: {
+			type: Boolean,
+			default: u.gc('loadingMoreEnabled', true)
+		},
+		// 是否启用滑动到底部加载更多数据,默认为是
+		toBottomLoadingMoreEnabled: {
+			type: Boolean,
+			default: u.gc('toBottomLoadingMoreEnabled', true)
+		},
+		// 滑动到底部状态为默认状态时,以加载中的状态展示,默认为否。若设置为是,可避免滚动到底部看到默认状态然后立刻变为加载中状态的问题,但分页数量未超过一屏时,不会显示【点击加载更多】
+		loadingMoreDefaultAsLoading: {
+			type: Boolean,
+			default: u.gc('loadingMoreDefaultAsLoading', false)
+		},
+		// 滑动到底部"默认"文字,默认为【点击加载更多】
+		loadingMoreDefaultText: {
+			type: [String, Object],
+			default: u.gc('loadingMoreDefaultText', null)
+		},
+		// 滑动到底部"加载中"文字,默认为【正在加载...】
+		loadingMoreLoadingText: {
+			type: [String, Object],
+			default: u.gc('loadingMoreLoadingText', null)
+		},
+		// 滑动到底部"没有更多"文字,默认为【没有更多了】
+		loadingMoreNoMoreText: {
+			type: [String, Object],
+			default: u.gc('loadingMoreNoMoreText', null)
+		},
+		// 滑动到底部"加载失败"文字,默认为【加载失败,点击重新加载】
+		loadingMoreFailText: {
+			type: [String, Object],
+			default: u.gc('loadingMoreFailText', null)
+		},
+		// 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为否
+		hideNoMoreInside: {
+			type: Boolean,
+			default: u.gc('hideNoMoreInside', false)
+		},
+		// 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0,代表不限制。
+		hideNoMoreByLimit: {
+			type: Number,
+			default: u.gc('hideNoMoreByLimit', 0)
+		},
+		// 是否显示默认的加载更多text,默认为是
+		showDefaultLoadingMoreText: {
+			type: Boolean,
+			default: u.gc('showDefaultLoadingMoreText', true)
+		},
+		// 是否显示没有更多数据的view
+		showLoadingMoreNoMoreView: {
+			type: Boolean,
+			default: u.gc('showLoadingMoreNoMoreView', true)
+		},
+		// 是否显示没有更多数据的分割线,默认为是
+		showLoadingMoreNoMoreLine: {
+			type: Boolean,
+			default: u.gc('showLoadingMoreNoMoreLine', true)
+		},
+		// 自定义底部没有更多数据的分割线样式
+		loadingMoreNoMoreLineCustomStyle: {
+			type: Object,
+			default: u.gc('loadingMoreNoMoreLineCustomStyle', {})
+		},
+		// 当分页未满一屏时,是否自动加载更多,默认为否(nvue无效)
+		insideMore: {
+			type: Boolean,
+			default: u.gc('insideMore', false)
+		},
+		// 距底部/右边多远时(单位px),触发 scrolltolower 事件,默认为100rpx
+		lowerThreshold: {
+			type: [Number, String],
+			default: u.gc('lowerThreshold', '100rpx')
+		},
+	},
+	data() {
+		return {
+			M: Enum.More,
+			// 底部加载更多状态
+			loadingStatus: Enum.More.Default,
+			// 在渲染之后的底部加载更多状态
+			loadingStatusAfterRender: Enum.More.Default,
+			// 底部加载更多时间戳
+			loadingMoreTimeStamp: 0,
+			// 底部加载更多slot
+			loadingMoreDefaultSlot: null,
+			// 是否展示底部加载更多
+			showLoadingMore: false,
+			// 是否是开发者自定义的加载更多,-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
+			customNoMore: -1,
+		}
+	},
+	computed: {
+		// 底部加载更多配置
+		zLoadMoreConfig() {
+			return {
+				status: this.loadingStatusAfterRender,
+				defaultAsLoading: this.loadingMoreDefaultAsLoading || (this.useChatRecordMode && this.chatLoadingMoreDefaultAsLoading),
+				defaultThemeStyle: this.finalLoadingMoreThemeStyle,
+				customStyle: this.loadingMoreCustomStyle,
+				titleCustomStyle: this.loadingMoreTitleCustomStyle,
+				iconCustomStyle: this.loadingMoreLoadingIconCustomStyle,
+				loadingIconType: this.loadingMoreLoadingIconType,
+				loadingIconCustomImage: this.loadingMoreLoadingIconCustomImage,
+				loadingAnimated: this.loadingMoreLoadingAnimated,
+				showNoMoreLine: this.showLoadingMoreNoMoreLine,
+				noMoreLineCustomStyle: this.loadingMoreNoMoreLineCustomStyle,
+				defaultText: this.finalLoadingMoreDefaultText,
+				loadingText: this.finalLoadingMoreLoadingText,
+				noMoreText: this.finalLoadingMoreNoMoreText,
+				failText: this.finalLoadingMoreFailText,
+				hideContent: !this.loadingMoreDefaultAsLoading && this.listRendering,
+				unit: this.unit,
+				isChat: this.useChatRecordMode,
+				chatDefaultAsLoading: this.chatLoadingMoreDefaultAsLoading
+			};
+		},
+		// 最终的底部加载更多主题
+		finalLoadingMoreThemeStyle() {
+			return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
+		},
+		// 最终的底部加载更多触发阈值
+		finalLowerThreshold() {
+			return u.convertToPx(this.lowerThreshold);
+		},
+		// 是否显示默认状态下的底部加载更多
+		showLoadingMoreDefault() {
+			return this._showLoadingMore('Default');
+		},
+		// 是否显示加载中状态下的底部加载更多
+		showLoadingMoreLoading() {
+			return this._showLoadingMore('Loading');
+		},
+		// 是否显示没有更多了状态下的底部加载更多
+		showLoadingMoreNoMore() {
+			return this._showLoadingMore('NoMore');
+		},
+		// 是否显示加载失败状态下的底部加载更多
+		showLoadingMoreFail() {
+			return this._showLoadingMore('Fail');
+		},
+		// 是否显示自定义状态下的底部加载更多
+		showLoadingMoreCustom() {
+			return this._showLoadingMore('Custom');
+		},
+		// 底部加载更多固定高度
+		loadingMoreFixedHeight() {
+			return u.addUnit('80rpx', this.unit);
+		},
+	},
+	methods: {
+		// 页面滚动到底部时通知z-paging进行进一步处理
+		pageReachBottom() {
+			!this.useChatRecordMode && this.toBottomLoadingMoreEnabled && this._onLoadingMore('toBottom');
+		},
+		// 手动触发上拉加载更多(非必须,可依据具体需求使用)
+		doLoadMore(type) {
+			this._onLoadingMore(type);
+		},
+		// 通过@scroll事件检测是否滚动到了底部(顺带检测下是否滚动到了顶部)
+		_checkScrolledToBottom(scrollDiff, checked = false) {
+			// 如果当前scroll-view高度未获取,则获取其高度
+			if (this.cacheScrollNodeHeight === -1) {
+				// 获取当前scroll-view高度
+				this._getNodeClientRect('.zp-scroll-view').then((res) => {
+					if (res) {
+						const scrollNodeHeight = res[0].height;
+						// 缓存当前scroll-view高度,如果获取过了不再获取
+						this.cacheScrollNodeHeight = scrollNodeHeight;
+						// // scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
+						if (scrollDiff - scrollNodeHeight <= this.finalLowerThreshold) {
+							// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
+							this._onLoadingMore('toBottom');
+						}
+					}
+				});
+			} else {
+				// scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
+				if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
+					// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
+					this._onLoadingMore('toBottom');
+				} else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
+					// 如果与底部的距离小于500px,则获取当前滚动的位置,延迟150毫秒重复上述步骤再次检测(避免@scroll触发时获取的scrollTop不正确导致的其他问题,此时获取的scrollTop不一定可信)。防止因为部分性能较差安卓设备@scroll采样率过低导致的滚动到底部但是依然没有触发的问题
+					u.delay(() => {
+						this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
+							if (res) {
+								this.oldScrollTop = res[0].scrollTop;
+								const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
+								this._checkScrolledToBottom(newScrollDiff, true);
+							}
+						})
+					}, 150, 'checkScrolledToBottomDelay')
+				}
+				// 检测一下是否已经滚动到了顶部了,因为在安卓中滚动到顶部时scrollTop不一定为0(和滚动到底部一样的原因),所以需要在scrollTop小于150px时,通过获取.zp-scroll-view的scrollTop再判断一下
+				if (this.oldScrollTop <= 150 && this.oldScrollTop !== 0) {
+					u.delay(() => {
+						// 这里再判断一下是否确实已经滚动到顶部了,如果已经滚动到顶部了,则不用再判断了,再次判断的原因是可能150毫秒之后oldScrollTop才是0
+						if (this.oldScrollTop !== 0) {
+							this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
+								// 如果150毫秒后.zp-scroll-view的scrollTop为0,则认为已经滚动到了顶部了
+								if (res && res[0].scrollTop === 0 && this.oldScrollTop !== 0) {
+									this._onScrollToUpper();
+								}
+							})
+						}
+					}, 150, 'checkScrolledToTopDelay')
+				}
+			}
+		},
+		// 触发加载更多时调用,from:toBottom-滑动到底部触发;click-点击加载更多触发
+		_onLoadingMore(from = 'click') {
+			// 如果是ios并且是滚动到底部的,则在滚动到底部时候尝试将列表设置为禁止滚动然后设置为允许滚动,以禁止底部bounce的效果
+			if (this.isIos && from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
+				this.scrollEnable = false;
+				this.$nextTick(() => {
+					this.scrollEnable = true;
+				})
+			}
+			// emit scrolltolower
+			this._emitScrollEvent('scrolltolower');
+			// 如果是只使用布局或下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则return,不触发内部加载更多逻辑
+			if (this.isOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
+			// #ifdef MP-WEIXIN
+			if (!this.isIos && !this.isOnly && !this.usePageScroll) {
+				const currentTimestamp = u.getTime();
+				// 在非ios平台+scroll-view中节流处理
+				if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
+					this.loadingMoreTimeStamp = 0;
+					return;
+				}
+			}
+			// #endif
+			// 处理加载更多数据
+			this._doLoadingMore();
+		},
+		// 处理开始加载更多
+		_doLoadingMore() {
+			if (this.pageNo >= this.defaultPageNo && this.loadingStatus !== Enum.More.NoMore) {
+				this.pageNo ++;
+				this._startLoading(false);
+				if (this.isLocalPaging) {
+					// 如果是本地分页,则在组件内部对数据进行分页处理,不触发@query事件
+					this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, res => {
+						this.completeByTotal(res, this.totalLocalPagingList.length);
+						this.queryFrom = Enum.QueryFrom.LoadMore;
+					})
+				} else {
+					// emit @query相关加载更多事件
+					this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadMore);
+					this._callMyParentQuery();
+				}
+				// 设置当前加载状态为底部加载更多状态
+				this.loadingType = Enum.LoadingType.LoadMore;
+			}
+		},
+		// (预处理)判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
+		_preCheckShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode) {
+			if (this.loadingStatus === Enum.More.NoMore && this.hideNoMoreByLimit > 0 && newVal.length) {
+				this.showLoadingMore = newVal.length > this.hideNoMoreByLimit;
+			} else if ((this.loadingStatus === Enum.More.NoMore && this.hideNoMoreInside && newVal.length) || (this.insideMore && this.insideOfPaging !== false && newVal.length)) {
+				this.$nextTick(() => {
+					this._checkShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode);
+				})
+				if (this.insideMore && this.insideOfPaging !== false && newVal.length) {
+					this.showLoadingMore = newVal.length;
+				}
+			} else {
+				this.showLoadingMore = newVal.length;
+			}
+		},
+		// 判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
+		async _checkShowNoMoreInside(totalData, oldScrollViewNode, oldPagingContainerNode) {
+			try {
+				const scrollViewNode = oldScrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
+				// 在页面滚动模式下
+				if (this.usePageScroll) {
+					if (scrollViewNode) {
+						// 获取滚动内容总高度
+						const scrollViewTotalH = scrollViewNode[0].top + scrollViewNode[0].height;
+						// 如果滚动内容总高度小于窗口高度,则认为内容未超出z-paging
+						this.insideOfPaging = scrollViewTotalH < this.windowHeight;
+						// 如果需要没有更多数据时,隐藏底部加载更多view,并且内容未超过z-paging,则隐藏底部加载更多
+						if (this.hideNoMoreInside) {
+							this.showLoadingMore = !this.insideOfPaging;
+						}
+						// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
+						this._updateInsideOfPaging();
+					}
+				} else {
+					// 在scroll-view滚动模式下
+					const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
+					// 获取滚动内容总高度
+					const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
+					// 获取z-paging内置scroll-view高度
+					const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
+					// 如果滚动内容总高度小于z-paging内置scroll-view高度,则认为内容未超出z-paging
+					this.insideOfPaging = pagingContainerH < scrollViewH;
+					if (this.hideNoMoreInside) {
+						this.showLoadingMore = !this.insideOfPaging;
+					}
+					// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
+					this._updateInsideOfPaging();
+				}
+			} catch (e) {
+				// 如果发生了异常,判断totalData数组长度为0,则认为内容未超出z-paging
+				this.insideOfPaging = !totalData.length;
+				if (this.hideNoMoreInside) {
+					this.showLoadingMore = !this.insideOfPaging;
+				}
+				// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
+				this._updateInsideOfPaging();
+			}
+		},
+		// 是否要展示上拉加载更多view
+		_showLoadingMore(type) {
+			if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
+			if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) || 
+			(!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.isOnly) {
+				return false;
+			}
+			if (this.useChatRecordMode && type !== 'Loading') return false;
+			if (!this.zSlots) return false;
+			if (type === 'Custom') {
+				return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
+			}
+			const res = this.loadingStatus === Enum.More[type] && this.zSlots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
+			if (res) {
+				// #ifdef APP-NVUE
+				if (!this.isIos) {
+					this.nLoadingMoreFixedHeight = false;
+				}
+				//  #endif
+			}
+			return res;
+		},
+	}
+}

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

@@ -0,0 +1,95 @@
+// [z-paging]loading相关模块
+import u from '.././z-paging-utils'
+import Enum from '.././z-paging-enum'
+
+export default {
+	props: {
+		// 第一次加载后自动隐藏loading slot,默认为是
+		autoHideLoadingAfterFirstLoaded: {
+			type: Boolean,
+			default: u.gc('autoHideLoadingAfterFirstLoaded', true)
+		},
+		// loading slot是否铺满屏幕并固定,默认为否
+		loadingFullFixed: {
+			type: Boolean,
+			default: u.gc('loadingFullFixed', false)
+		},
+		// 是否自动显示系统Loading:即uni.showLoading,若开启则将在刷新列表时(调用reload、refresh时)显示,下拉刷新和滚动到底部加载更多不会显示,默认为false。
+		autoShowSystemLoading: {
+			type: Boolean,
+			default: u.gc('autoShowSystemLoading', false)
+		},
+		// 显示系统Loading时是否显示透明蒙层,防止触摸穿透,默认为是(H5、App、微信小程序、百度小程序有效)
+		systemLoadingMask: {
+			type: Boolean,
+			default: u.gc('systemLoadingMask', true)
+		},
+		// 显示系统Loading时显示的文字,默认为"加载中"
+		systemLoadingText: {
+			type: [String, Object],
+			default: u.gc('systemLoadingText', null)
+		},
+	},
+	data() {
+		return {
+			loading: false,
+			loadingForNow: false,
+		}
+	},
+	watch: {
+		// loading状态
+		loadingStatus(newVal) {
+			this.$emit('loadingStatusChange', newVal);
+			this.$nextTick(() => {
+				this.loadingStatusAfterRender = newVal;
+			})
+			if (this.useChatRecordMode) {
+				if (this.isFirstPage && (newVal === Enum.More.NoMore || newVal === Enum.More.Fail)) {
+					this.isFirstPageAndNoMore = true;
+					return;
+				}
+			}
+			this.isFirstPageAndNoMore = false;
+		},
+		loading(newVal){
+			if (newVal) {
+				this.loadingForNow = newVal;
+			}
+		},
+	},
+	computed: {
+		// 是否显示loading
+		showLoading() {
+			if (this.firstPageLoaded || !this.loading || !this.loadingForNow) return false;
+			if (this.finalShowSystemLoading) {
+				// 显示系统loading
+				uni.showLoading({
+					title: this.finalSystemLoadingText,
+					mask: this.systemLoadingMask
+				})
+			}
+			return this.autoHideLoadingAfterFirstLoaded ? (this.fromEmptyViewReload ? true : !this.pagingLoaded) : this.loadingType === Enum.LoadingType.Refresher;
+		},
+		// 最终的是否显示系统loading
+		finalShowSystemLoading() {
+			return this.autoShowSystemLoading && this.loadingType === Enum.LoadingType.Refresher;
+		}
+	},
+	methods: {
+		// 处理开始加载更多状态
+		_startLoading(isReload = false) {
+			if ((this.showLoadingMoreWhenReload && !this.isUserPullDown) || !isReload) {
+				this.loadingStatus = Enum.More.Loading;
+			}
+			this.loading = true;
+		},
+		// 停止系统loading和refresh
+		_endSystemLoadingAndRefresh(){
+			this.finalShowSystemLoading && uni.hideLoading();
+			!this.useCustomRefresher && uni.stopPullDownRefresh();
+			// #ifdef APP-NVUE
+			this.usePageScroll && uni.stopPullDownRefresh();
+			// #endif
+		}
+	}
+}

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

@@ -0,0 +1,299 @@
+// [z-paging]nvue独有部分模块
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import Enum from '.././z-paging-enum'
+
+// #ifdef APP-NVUE
+const weexAnimation = weex.requireModule('animation');
+// #endif
+export default {
+	props: {
+		// #ifdef APP-NVUE
+		// nvue中修改列表类型,可选值有list、waterfall和scroller,默认为list
+		nvueListIs: {
+			type: String,
+			default: u.gc('nvueListIs', 'list')
+		},
+		// nvue waterfall配置,仅在nvue中且nvueListIs=waterfall时有效,配置参数详情参见:https://uniapp.dcloud.io/component/waterfall
+		nvueWaterfallConfig: {
+			type: Object,
+			default: u.gc('nvueWaterfallConfig', {})
+		},
+		// nvue 控制是否回弹效果,iOS不支持动态修改
+		nvueBounce: {
+			type: Boolean,
+			default: u.gc('nvueBounce', true)
+		},
+		// nvue中通过代码滚动到顶部/底部时,是否加快动画效果(无滚动动画时无效),默认为否
+		nvueFastScroll: {
+			type: Boolean,
+			default: u.gc('nvueFastScroll', false)
+		},
+		// nvue中list的id
+		nvueListId: {
+			type: String,
+			default: u.gc('nvueListId', '')
+		},
+		// nvue中refresh组件的样式
+		nvueRefresherStyle: {
+			type: Object,
+			default: u.gc('nvueRefresherStyle', {})
+		},
+		// nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
+		nvuePagingEnabled: {
+			type: Boolean,
+			default: u.gc('nvuePagingEnabled', false)
+		},
+		// 是否隐藏nvue列表底部的tagView,此view用于标识滚动到底部位置,若隐藏则滚动到底部功能将失效,在nvue中实现吸顶+swiper功能时需将最外层z-paging的此属性设置为true。默认为否
+		hideNvueBottomTag: {
+			type: Boolean,
+			default: u.gc('hideNvueBottomTag', false)
+		},
+		// nvue中控制onscroll事件触发的频率:表示两次onscroll事件之间列表至少滚动了10px。注意,将该值设置为较小的数值会提高滚动事件采样的精度,但同时也会降低页面的性能
+		offsetAccuracy: {
+			type: Number,
+			default: u.gc('offsetAccuracy', 10)
+		},
+		// #endif
+	},
+	data() {
+		return {
+			nRefresherLoading: false,
+			nListIsDragging: false,
+			nShowBottom: true,
+			nFixFreezing: false,
+			nShowRefresherReveal: false,
+			nLoadingMoreFixedHeight: false,
+			nShowRefresherRevealHeight: 0,
+			nOldShowRefresherRevealHeight: -1,
+			nRefresherWidth: u.rpx2px(750),
+			nListHeight: 0,
+			nF2Opacity: 0
+		}
+	},
+	computed: {
+		// #ifdef APP-NVUE
+		nScopedSlots() {
+			// #ifdef VUE2
+			return this.$scopedSlots;
+			// #endif
+			// #ifdef VUE3
+			return null;
+			// #endif
+		},
+		nWaterfallColumnCount() {
+			if (this.finalNvueListIs !== 'waterfall') return 0;
+			return this._nGetWaterfallConfig('column-count', 2);
+		},
+		nWaterfallColumnWidth() {
+			return this._nGetWaterfallConfig('column-width', 'auto');
+		},
+		nWaterfallColumnGap() {
+			return this._nGetWaterfallConfig('column-gap', 'normal');
+		},
+		nWaterfallLeftGap() {
+			return this._nGetWaterfallConfig('left-gap', 0);
+		},
+		nWaterfallRightGap() {
+			return this._nGetWaterfallConfig('right-gap', 0);
+		},
+		nViewIs() {
+			const is = this.finalNvueListIs;
+			return is === 'scroller' || is === 'view' ? 'view' : is === 'waterfall' ? 'header' : 'cell';
+		},
+		nSafeAreaBottomHeight() {
+			return this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
+		},
+		finalNvueListIs() {
+			if (this.usePageScroll) return 'view';
+			const nvueListIsLowerCase = this.nvueListIs.toLowerCase();
+			if (['list','waterfall','scroller'].indexOf(nvueListIsLowerCase) !== -1) return nvueListIsLowerCase;
+			return 'list';
+		},
+		finalNvueSuperListIs() {
+			return this.usePageScroll ? 'view' : 'scroller';
+		},
+		finalNvueRefresherEnabled() {
+			return this.finalNvueListIs !== 'view' && this.finalRefresherEnabled && !this.nShowRefresherReveal && !this.useChatRecordMode;
+		},
+		// #endif
+	},
+	mounted(){
+		// #ifdef APP-NVUE
+		//旋转屏幕时更新宽度
+		uni.onWindowResize((res) => {
+			// this._nUpdateRefresherWidth();
+		})
+		// #endif
+	},
+	methods: {
+		// #ifdef APP-NVUE
+		// 列表滚动时触发
+		_nOnScroll(e) {
+			this.$emit('scroll', e);
+			const scrollTop = -e.contentOffset.y;
+			const scrollHeight = e.contentSize.height;
+			
+			if (this.watchScrollDirectionChange) {
+				// 计算scroll-view滚动方向,正常情况下上次滚动的oldScrollTop大于当前scrollTop即为向上滚动,反之为向下滚动
+				let direction = this.oldScrollTop > scrollTop ? 'top' : 'bottom';
+				// 此处为解决在iOS中,滚动到顶部因bounce的影响回弹导致滚动方向为bottom的问题:如果滚动到顶部了并且scrollTop小于顶部滚动区域,则强制设置direction为top
+				if (scrollTop <= 0) {
+					direction = 'top';
+				}
+				// 此处为解决在iOS中,滚动到底部因bounce的影响回弹导致滚动方向为top的问题:如果滚动到底部了并且scrollTop超过底部滚动区域,则强制设置direction为bottom
+				if (scrollTop > this.lastScrollHeight - this.nListHeight - 1) {
+					direction = 'bottom';
+				}
+				// emit 列表滚动方向改变事件
+				if (direction !== this.lastScrollDirection) {
+					this.$emit('scrollDirectionChange', direction);
+					this.lastScrollDirection = direction;
+				}
+				// 当scrollHeight变化时,需要延迟100毫秒设置lastScrollHeight,如果直接根据scrollHeight的话,因为此时数据还未改变,会导致滚动方向从bottom变为top
+				if (this.lastScrollHeight !== scrollHeight && !this.setContentHeightPending) {
+					// 因此处会多次触发,因此加个标识确保在延时期间仅触发一次
+					this.setContentHeightPending = true;
+					u.delay(() => {
+						this.lastScrollHeight = scrollHeight;
+						this.setContentHeightPending = false;
+					})
+				}
+			}
+			
+			this.oldScrollTop = scrollTop;
+			this.nListIsDragging = e.isDragging;
+			this._checkShouldShowBackToTop(scrollTop, scrollTop - 1);
+		},
+		// 列表滚动结束
+		_nOnScrollend(e) {
+			this.$emit('scrollend', e);
+			
+			// 判断是否滚动到顶部了
+			if (e?.contentOffset?.y >= 0) {
+				this._emitScrollEvent('scrolltoupper');
+			}
+			// 判断是否滚动到底部了
+			this._getNodeClientRect('.zp-n-list').then(node => {
+				if (node) {
+					this.nListHeight = node[0].height;
+					if (e?.contentSize?.height + e?.contentOffset?.y <= node[0].height) {
+						this._emitScrollEvent('scrolltolower');
+					}
+				}
+			})
+		},
+		// 下拉刷新刷新中
+		_nOnRrefresh() {
+			if (this.nShowRefresherReveal) return;
+			// 进入刷新状态
+			this.nRefresherLoading = true;
+			if (this.refresherStatus === Enum.Refresher.GoF2) {
+				this._handleGoF2();
+				this.$nextTick(() => {
+					this._nRefresherEnd();
+				})
+			} else {
+				this.refresherStatus = Enum.Refresher.Loading;
+				this._doRefresherLoad();
+			}
+			
+		},
+		// 下拉刷新下拉中
+		_nOnPullingdown(e) {
+			if (this.refresherStatus === Enum.Refresher.Loading || (this.isIos && !this.nListIsDragging)) return;
+			this._emitTouchmove(e);
+			let { viewHeight, pullingDistance } = e;
+			// 更新下拉刷新状态
+			// 下拉刷新距离超过阈值
+			if (pullingDistance >= viewHeight) {
+				// 如果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
+				// (pullingDistance - viewHeight) + this.finalRefresherThreshold 不等同于pullingDistance,此处是为了兼容不同平台下拉相同距离pullingDistance不一致的问题,pullingDistance仅与viewHeight互相关联
+				this.refresherStatus = this.refresherF2Enabled && (pullingDistance - viewHeight) + this.finalRefresherThreshold >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
+			} else {
+				// 下拉刷新距离未超过阈值,显示默认状态
+				this.refresherStatus = Enum.Refresher.Default;
+			}
+		},
+		// 下拉刷新结束
+		_nRefresherEnd(doEnd = true) {
+			if (doEnd) {
+			   this._nDoRefresherEndAnimation(0, -this.nShowRefresherRevealHeight); 
+			   !this.usePageScroll && this.$refs['zp-n-list'].resetLoadmore();
+			   this.nRefresherLoading = false;
+			}
+		},
+		// 执行主动触发下拉刷新动画
+		_nDoRefresherEndAnimation(height, translateY, animate = true, checkStack = true) {
+			// 清除下拉刷新相关timeout
+			this._cleanRefresherCompleteTimeout();
+			this._cleanRefresherEndTimeout();
+			
+			if (!this.finalShowRefresherWhenReload) {
+				// 如果reload不需要自动展示下拉刷新view,则在complete duration结束后再把下拉刷新状态设置回默认
+				this.refresherEndTimeout = u.delay(() => {
+					this.refresherStatus = Enum.Refresher.Default;
+				}, this.refresherCompleteDuration);
+				return;
+			}
+			// 用户处理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
+			const stackCount = this.refresherRevealStackCount;
+			if (height === 0 && checkStack) {
+				this.refresherRevealStackCount --;
+				if (stackCount > 1) return;
+				this.refresherEndTimeout = u.delay(() => {
+					this.refresherStatus = Enum.Refresher.Default;
+				}, this.refresherCompleteDuration);
+			}
+			if (stackCount > 1) {
+				this.refresherStatus = Enum.Refresher.Loading;
+			}
+			
+			const duration = animate ? 200 : 0;
+			if (this.nOldShowRefresherRevealHeight !== height) {
+				if (height > 0) {
+					this.nShowRefresherReveal = true;
+				}
+				// 展示下拉刷新view
+				weexAnimation.transition(this.$refs['zp-n-list-refresher-reveal'], {
+					styles: {
+						height: `${height}px`,
+						transform: `translateY(${translateY}px)`,
+					},
+					duration,
+					timingFunction: 'linear',
+					needLayout: true,
+					delay: 0
+				})
+			}
+			u.delay(() => {
+				if (animate) {
+					this.nShowRefresherReveal = height > 0;
+				}
+			}, duration > 0 ? duration - 60 : 0);
+			this.nOldShowRefresherRevealHeight = height;
+		},
+		// 滚动到底部加载更多
+		_nOnLoadmore() {
+			if (this.nShowRefresherReveal || !this.totalData.length) return;
+			this.useChatRecordMode ? this.doChatRecordLoadMore() : this._onLoadingMore('toBottom');
+		},
+		// 获取nvue waterfall单项配置
+		_nGetWaterfallConfig(key, defaultValue) {
+			return this.nvueWaterfallConfig[key] || defaultValue;
+		},
+		// 更新nvue 下拉刷新view容器的宽度
+		_nUpdateRefresherWidth() {
+			u.delay(() => {
+				this.$nextTick(()=>{
+					this._getNodeClientRect('.zp-n-list').then(node => {
+						if (node) {
+							this.nRefresherWidth = node[0].width || this.nRefresherWidth;
+						}
+					})
+				})
+			})	
+		}
+		// #endif
+	}
+}

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

@@ -0,0 +1,835 @@
+// [z-paging]下拉刷新view模块
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import Enum from '.././z-paging-enum'
+
+// #ifdef APP-NVUE
+const weexAnimation = weex.requireModule('animation');
+// #endif
+export default {
+	props: {
+		// 下拉刷新的主题样式,支持black,white,默认black
+		refresherThemeStyle: {
+			type: String,
+			default: u.gc('refresherThemeStyle', '')
+		},
+		// 自定义下拉刷新中左侧图标的样式
+		refresherImgStyle: {
+			type: Object,
+			default: u.gc('refresherImgStyle', {})
+		},
+		// 自定义下拉刷新中右侧状态描述文字的样式
+		refresherTitleStyle: {
+			type: Object,
+			default: u.gc('refresherTitleStyle', {})
+		},
+		// 自定义下拉刷新中右侧最后更新时间文字的样式(show-refresher-update-time为true时有效)
+		refresherUpdateTimeStyle: {
+			type: Object,
+			default: u.gc('refresherUpdateTimeStyle', {})
+		},
+		// 在微信小程序和QQ小程序中,是否实时监听下拉刷新中进度,默认为否
+		watchRefresherTouchmove: {
+			type: Boolean,
+			default: u.gc('watchRefresherTouchmove', false)
+		},
+		// 底部加载更多的主题样式,支持black,white,默认black
+		loadingMoreThemeStyle: {
+			type: String,
+			default: u.gc('loadingMoreThemeStyle', '')
+		},
+		// 是否只使用下拉刷新,设置为true后将关闭mounted自动请求数据、关闭滚动到底部加载更多,强制隐藏空数据图。默认为否
+		refresherOnly: {
+			type: Boolean,
+			default: u.gc('refresherOnly', false)
+		},
+		// 自定义下拉刷新默认状态下回弹动画时间,单位为毫秒,默认为100毫秒,nvue无效
+		refresherDefaultDuration: {
+			type: [Number, String],
+			default: u.gc('refresherDefaultDuration', 100)
+		},
+		// 自定义下拉刷新结束以后延迟回弹的时间,单位为毫秒,默认为0
+		refresherCompleteDelay: {
+			type: [Number, String],
+			default: u.gc('refresherCompleteDelay', 0)
+		},
+		// 自定义下拉刷新结束回弹动画时间,单位为毫秒,默认为300毫秒(refresherEndBounceEnabled为false时,refresherCompleteDuration为设定值的1/3),nvue无效
+		refresherCompleteDuration: {
+			type: [Number, String],
+			default: u.gc('refresherCompleteDuration', 300)
+		},
+		// 自定义下拉刷新中是否允许列表滚动,默认为是
+		refresherRefreshingScrollable: {
+			type: Boolean,
+			default: u.gc('refresherRefreshingScrollable', true)
+		},
+		// 自定义下拉刷新结束状态下是否允许列表滚动,默认为否
+		refresherCompleteScrollable: {
+			type: Boolean,
+			default: u.gc('refresherCompleteScrollable', false)
+		},
+		// 是否使用自定义的下拉刷新,默认为是,即使用z-paging的下拉刷新。设置为false即代表使用uni scroll-view自带的下拉刷新,h5、App、微信小程序以外的平台不支持uni scroll-view自带的下拉刷新
+		useCustomRefresher: {
+			type: Boolean,
+			default: u.gc('useCustomRefresher', true)
+		},
+		// 自定义下拉刷新下拉帧率,默认为40,过高可能会出现抖动问题
+		refresherFps: {
+			type: [Number, String],
+			default: u.gc('refresherFps', 40)
+		},
+		// 自定义下拉刷新允许触发的最大下拉角度,默认为40度,当下拉角度小于设定值时,自定义下拉刷新动画不会被触发
+		refresherMaxAngle: {
+			type: [Number, String],
+			default: u.gc('refresherMaxAngle', 40)
+		},
+		// 自定义下拉刷新的角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为否
+		refresherAngleEnableChangeContinued: {
+			type: Boolean,
+			default: u.gc('refresherAngleEnableChangeContinued', false)
+		},
+		// 自定义下拉刷新默认状态下的文字
+		refresherDefaultText: {
+			type: [String, Object],
+			default: u.gc('refresherDefaultText', null)
+		},
+		// 自定义下拉刷新松手立即刷新状态下的文字
+		refresherPullingText: {
+			type: [String, Object],
+			default: u.gc('refresherPullingText', null)
+		},
+		// 自定义下拉刷新刷新中状态下的文字
+		refresherRefreshingText: {
+			type: [String, Object],
+			default: u.gc('refresherRefreshingText', null)
+		},
+		// 自定义下拉刷新刷新结束状态下的文字
+		refresherCompleteText: {
+			type: [String, Object],
+			default: u.gc('refresherCompleteText', null)
+		},
+		// 自定义继续下拉进入二楼文字
+		refresherGoF2Text: {
+			type: [String, Object],
+			default: u.gc('refresherGoF2Text', null)
+		},
+		// 自定义下拉刷新默认状态下的图片
+		refresherDefaultImg: {
+			type: String,
+			default: u.gc('refresherDefaultImg', null)
+		},
+		// 自定义下拉刷新松手立即刷新状态下的图片,默认与refresherDefaultImg一致
+		refresherPullingImg: {
+			type: String,
+			default: u.gc('refresherPullingImg', null)
+		},
+		// 自定义下拉刷新刷新中状态下的图片
+		refresherRefreshingImg: {
+			type: String,
+			default: u.gc('refresherRefreshingImg', null)
+		},
+		// 自定义下拉刷新刷新结束状态下的图片
+		refresherCompleteImg: {
+			type: String,
+			default: u.gc('refresherCompleteImg', null)
+		},
+		// 自定义下拉刷新刷新中状态下是否展示旋转动画
+		refresherRefreshingAnimated: {
+			type: Boolean,
+			default: u.gc('refresherRefreshingAnimated', true)
+		},
+		// 是否开启自定义下拉刷新刷新结束回弹效果,默认为是
+		refresherEndBounceEnabled: {
+			type: Boolean,
+			default: u.gc('refresherEndBounceEnabled', true)
+		},
+		// 是否开启自定义下拉刷新,默认为是
+		refresherEnabled: {
+			type: Boolean,
+			default: u.gc('refresherEnabled', true)
+		},
+		// 设置自定义下拉刷新阈值,默认为80rpx
+		refresherThreshold: {
+			type: [Number, String],
+			default: u.gc('refresherThreshold', '80rpx')
+		},
+		// 设置系统下拉刷新默认样式,支持设置 black,white,none,none 表示不使用默认样式,默认为black
+		refresherDefaultStyle: {
+			type: String,
+			default: u.gc('refresherDefaultStyle', 'black')
+		},
+		// 设置自定义下拉刷新区域背景
+		refresherBackground: {
+			type: String,
+			default: u.gc('refresherBackground', 'transparent')
+		},
+		// 设置固定的自定义下拉刷新区域背景
+		refresherFixedBackground: {
+			type: String,
+			default: u.gc('refresherFixedBackground', 'transparent')
+		},
+		// 设置固定的自定义下拉刷新区域高度,默认为0
+		refresherFixedBacHeight: {
+			type: [Number, String],
+			default: u.gc('refresherFixedBacHeight', 0)
+		},
+		// 设置自定义下拉刷新下拉超出阈值后继续下拉位移衰减的比例,范围0-1,值越大代表衰减越多。默认为0.65(nvue无效)
+		refresherOutRate: {
+			type: Number,
+			default: u.gc('refresherOutRate', 0.65)
+		},
+		// 是否开启下拉进入二楼功能,默认为否
+		refresherF2Enabled: {
+			type: Boolean,
+			default: u.gc('refresherF2Enabled', false)
+		},
+		// 下拉进入二楼阈值,默认为200rpx
+		refresherF2Threshold: {
+			type: [Number, String],
+			default: u.gc('refresherF2Threshold', '200rpx')
+		},
+		// 下拉进入二楼动画时间,单位为毫秒,默认为200毫秒
+		refresherF2Duration: {
+			type: [Number, String],
+			default: u.gc('refresherF2Duration', 200)
+		},
+		// 下拉进入二楼状态松手后是否弹出二楼,默认为是
+		showRefresherF2: {
+			type: Boolean,
+			default: u.gc('showRefresherF2', true)
+		},
+		// 设置自定义下拉刷新下拉时实际下拉位移与用户下拉距离的比值,默认为0.75,即代表若用户下拉10px,则实际位移为7.5px(nvue无效)
+		refresherPullRate: {
+			type: Number,
+			default: u.gc('refresherPullRate', 0.75)
+		},
+		// 是否显示最后更新时间,默认为否
+		showRefresherUpdateTime: {
+			type: Boolean,
+			default: u.gc('showRefresherUpdateTime', false)
+		},
+		// 如果需要区别不同页面的最后更新时间,请为不同页面的z-paging的`refresher-update-time-key`设置不同的字符串
+		refresherUpdateTimeKey: {
+			type: String,
+			default: u.gc('refresherUpdateTimeKey', 'default')
+		},
+		// 下拉刷新时下拉到“松手立即刷新”或“松手进入二楼”状态时是否使手机短振动,默认为否(h5无效)
+		refresherVibrate: {
+			type: Boolean,
+			default: u.gc('refresherVibrate', false)
+		},
+		// 下拉刷新时是否禁止下拉刷新view跟随用户触摸竖直移动,默认为否。注意此属性只是禁止下拉刷新view移动,其他下拉刷新逻辑依然会正常触发
+		refresherNoTransform: {
+			type: Boolean,
+			default: u.gc('refresherNoTransform', false)
+		},
+		// 是否开启下拉刷新状态栏占位,适用于隐藏导航栏时,下拉刷新需要避开状态栏高度的情况,默认为否
+		useRefresherStatusBarPlaceholder: {
+			type: Boolean,
+			default: u.gc('useRefresherStatusBarPlaceholder', false)
+		},
+	},
+	data() {
+		return {
+			R: Enum.Refresher,
+			//下拉刷新状态
+			refresherStatus: Enum.Refresher.Default,
+			refresherTouchstartY: 0,
+			lastRefresherTouchmove: null,
+			refresherReachMaxAngle: true,
+			refresherTransform: 'translateY(0px)',
+			refresherTransition: '',
+			finalRefresherDefaultStyle: 'black',
+			refresherRevealStackCount: 0,
+			refresherCompleteTimeout: null,
+			refresherCompleteSubTimeout: null,
+			refresherEndTimeout: null,
+			isTouchmovingTimeout: null,
+			refresherTriggered: false,
+			isTouchmoving: false,
+			isTouchEnded: false,
+			isUserPullDown: false,
+			privateRefresherEnabled: -1,
+			privateShowRefresherWhenReload: false,
+			customRefresherHeight: -1,
+			showCustomRefresher: false,
+			doRefreshAnimateAfter: false,
+			isRefresherInComplete: false,
+			showF2: false,
+			f2Transform: '',
+			pullDownTimeStamp: 0,
+			moveDis: 0,
+			oldMoveDis: 0,
+			currentDis: 0,
+			oldCurrentMoveDis: 0,
+			oldRefresherTouchmoveY: 0,
+			oldTouchDirection: '',
+			oldEmitedTouchDirection: '',
+			oldPullingDistance: -1,
+			refresherThresholdUpdateTag: 0
+		}
+	},
+	watch: {
+		refresherDefaultStyle: {
+			handler(newVal) {
+				if (newVal.length) {
+					this.finalRefresherDefaultStyle = newVal;
+				}
+			},
+			immediate: true
+		},
+		refresherStatus(newVal) {
+			newVal === Enum.Refresher.Loading && this._cleanRefresherEndTimeout();
+			this.refresherVibrate && (newVal === Enum.Refresher.ReleaseToRefresh || newVal === Enum.Refresher.GoF2) && this._doVibrateShort();
+			this.$emit('refresherStatusChange', newVal);
+			this.$emit('update:refresherStatus', newVal);
+		},
+		// 监听当前下拉刷新启用/禁用状态
+		refresherEnabled(newVal) {
+			// 当禁用下拉刷新时,强制收回正在展示的下拉刷新view
+			!newVal && this.endRefresh();
+		}
+	},
+	computed: {
+		pullDownDisTimeStamp() {
+			return 1000 / this.refresherFps;
+		},
+		refresherThresholdUnitConverted() {
+			return u.addUnit(this.refresherThreshold, this.unit);
+		},
+		finalRefresherEnabled() {
+			if (this.layoutOnly || this.useChatRecordMode) return false;
+			if (this.privateRefresherEnabled === -1) return this.refresherEnabled;
+			return this.privateRefresherEnabled === 1;
+		},
+		finalRefresherThreshold() {
+			let refresherThreshold = this.refresherThresholdUnitConverted;
+			let idDefault = false;
+			if (refresherThreshold === u.addUnit(80, this.unit)) {
+				idDefault = true;
+				if (this.showRefresherUpdateTime) {
+					refresherThreshold = u.addUnit(120, this.unit);
+				}
+			}
+			if (idDefault && this.customRefresherHeight > 0) return this.customRefresherHeight + this.finalRefresherThresholdPlaceholder;
+			return u.convertToPx(refresherThreshold) + this.finalRefresherThresholdPlaceholder;
+		},
+		finalRefresherF2Threshold() {
+			return u.convertToPx(u.addUnit(this.refresherF2Threshold, this.unit));
+		},
+		finalRefresherThresholdPlaceholder() {
+			return this.useRefresherStatusBarPlaceholder ? this.statusBarHeight : 0;
+		},
+		finalRefresherFixedBacHeight() {
+			return u.convertToPx(this.refresherFixedBacHeight);
+		},
+		finalRefresherThemeStyle() {
+			return this.refresherThemeStyle.length ? this.refresherThemeStyle : this.defaultThemeStyle;
+		},
+		finalRefresherOutRate() {
+			let rate = this.refresherOutRate;
+			rate = Math.max(0,rate);
+			rate = Math.min(1,rate);
+			return rate;
+		},
+		finalRefresherPullRate() {
+			let rate = this.refresherPullRate;
+			rate = Math.max(0,rate);
+			return rate;
+		},
+		finalRefresherTransform() {
+			if (this.refresherNoTransform || this.refresherTransform === 'translateY(0px)') return 'none';
+			return this.refresherTransform;
+		},
+		finalShowRefresherWhenReload() {
+			return this.showRefresherWhenReload || this.privateShowRefresherWhenReload;
+		},
+		finalRefresherTriggered() {
+			if (!(this.finalRefresherEnabled && !this.useCustomRefresher)) return false;
+			return this.refresherTriggered;
+		},
+		showRefresher() {
+			const showRefresher = this.finalRefresherEnabled || this.useCustomRefresher && !this.useChatRecordMode;
+			// #ifndef APP-NVUE
+			this.active && this.customRefresherHeight === -1 && showRefresher && this.updateCustomRefresherHeight();
+			// #endif
+			return showRefresher;
+		},
+		hasTouchmove() {
+			// #ifdef VUE2
+			// #ifdef APP-VUE || H5
+			if (this.$listeners && !this.$listeners.refresherTouchmove) return false;
+			// #endif
+			// #ifdef MP-WEIXIN || MP-QQ
+			return this.watchRefresherTouchmove;
+			// #endif
+			return true;
+			// #endif
+			return this.watchRefresherTouchmove;
+		},
+	},
+	methods: {
+		// 终止下拉刷新状态
+		endRefresh() {
+			this.totalData = this.realTotalData;
+			this._refresherEnd();
+			this._endSystemLoadingAndRefresh();
+			this._handleScrollViewBounce({ bounce: true });
+			this.$nextTick(() => {
+				this.refresherTriggered = false;
+			})
+		},
+		// 手动更新自定义下拉刷新view高度
+		updateCustomRefresherHeight() {
+			u.delay(() => this.$nextTick(this._updateCustomRefresherHeight));
+		},
+		// 进入二楼
+		goF2() {
+			this._handleGoF2();
+		},
+		// 关闭二楼
+		closeF2() {
+			this._handleCloseF2();
+		},
+		// 自定义下拉刷新被触发
+		_onRefresh(fromScrollView = false, isUserPullDown = true) {
+			if (fromScrollView && !(this.finalRefresherEnabled && !this.useCustomRefresher)) return;
+			this.$emit('onRefresh');
+			this.$emit('Refresh');
+			// #ifdef APP-NVUE
+			if (this.loading) {
+				u.delay(this._nRefresherEnd, 500)
+				return;
+			}
+			// #endif
+			if (this.loading || this.isRefresherInComplete) return;
+			this.loadingType = Enum.LoadingType.Refresher;
+			if (this.nShowRefresherReveal) return;
+			this.isUserPullDown = isUserPullDown;
+			this.isUserReload = !isUserPullDown;
+			this._startLoading(true);
+			this.refresherTriggered = true;
+			if (this.reloadWhenRefresh && isUserPullDown) {
+				this.useChatRecordMode ? this._onLoadingMore('click') : this._reload(false, false, isUserPullDown);
+			}
+		},
+		// 自定义下拉刷新被复位
+		_onRestore() {
+			this.refresherTriggered = 'restore';
+			this.$emit('onRestore');
+			this.$emit('Restore');
+		},
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		// touch开始
+		_refresherTouchstart(e) {
+			this._handleListTouchstart();
+			if (this._touchDisabled()) return;
+			this._handleRefresherTouchstart(u.getTouch(e));
+		},
+		// #endif
+		// 进一步处理touch开始结果
+		_handleRefresherTouchstart(touch) {
+			if (!this.loading && this.isTouchEnded) {
+				this.isTouchmoving = false;
+			}
+			this.loadingType = Enum.LoadingType.Refresher;
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this.isTouchEnded = false;
+			this.refresherTransition = '';
+			this.refresherTouchstartY = touch.touchY;
+			this.$emit('refresherTouchstart', this.refresherTouchstartY);
+			this.lastRefresherTouchmove = touch;
+			this._cleanRefresherCompleteTimeout();
+			this._cleanRefresherEndTimeout();
+		},
+		
+		// 非app-vue或微信小程序或QQ小程序或h5平台,使用js控制下拉刷新
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		// touch中
+		_refresherTouchmove(e) {
+			const currentTimeStamp = u.getTime();
+			let touch = null;
+			let refresherTouchmoveY = 0;
+			if (this.watchTouchDirectionChange) {
+				// 检测下拉刷新方向改变
+				touch = u.getTouch(e);
+				refresherTouchmoveY = touch.touchY;
+				const direction  = refresherTouchmoveY > this.oldRefresherTouchmoveY ? 'top' : 'bottom';
+				// 只有在方向改变的时候才emit相关事件
+				if (direction === this.oldTouchDirection && direction !== this.oldEmitedTouchDirection) {
+					this._handleTouchDirectionChange({ direction });
+					this.oldEmitedTouchDirection = direction;
+				}
+				this.oldTouchDirection = direction;
+				this.oldRefresherTouchmoveY = refresherTouchmoveY;
+			}
+			// 节流处理,在pullDownDisTimeStamp时间内的下拉刷新中事件不进行处理
+			if (this.pullDownTimeStamp && currentTimeStamp - this.pullDownTimeStamp <= this.pullDownDisTimeStamp) return;
+			// 如果不允许下拉,则return
+			if (this._touchDisabled()) return;
+			this.pullDownTimeStamp = Number(currentTimeStamp);
+			touch = u.getTouch(e);
+			refresherTouchmoveY = touch.touchY;
+			// 获取当前touch的y - 初始touch的y,计算它们的差
+			let moveDis = refresherTouchmoveY - this.refresherTouchstartY;
+			if (moveDis < 0) return;
+			// 对下拉刷新的角度进行限制
+			if (this.refresherMaxAngle >= 0 && this.refresherMaxAngle <= 90 && this.lastRefresherTouchmove && this.lastRefresherTouchmove.touchY <= refresherTouchmoveY) {
+				if (!moveDis && !this.refresherAngleEnableChangeContinued && this.moveDis < 1 && !this.refresherReachMaxAngle) return;
+				const x = Math.abs(touch.touchX - this.lastRefresherTouchmove.touchX);
+				const y = Math.abs(refresherTouchmoveY - this.lastRefresherTouchmove.touchY);
+				const z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+				if ((x || y) && x > 1) {
+					// 获取下拉刷新前后两次位移的角度
+					const angle = Math.asin(y / z) / Math.PI * 180;
+					// 如果角度小于配置要求,则return
+					if (angle < this.refresherMaxAngle) {
+						this.lastRefresherTouchmove = touch;
+						this.refresherReachMaxAngle = false;
+						return;
+					}
+				}
+			}
+			// 获取最终的moveDis
+			moveDis = this._getFinalRefresherMoveDis(moveDis);
+			// 处理下拉刷新位移
+			this._handleRefresherTouchmove(moveDis, touch);
+			// 下拉刷新时,禁止页面滚动以防止页面向下滚动和下拉刷新同时作用导致下拉刷新位置偏移超过预期
+			if (!this.disabledBounce) {
+				// #ifndef MP-LARK
+				this._handleScrollViewBounce({ bounce: false });
+				// #endif
+				this.disabledBounce = true;
+			}
+			this._emitTouchmove({ pullingDistance: moveDis, dy: this.moveDis - this.oldMoveDis });
+		},
+		// #endif
+		// 进一步处理touch中结果
+		_handleRefresherTouchmove(moveDis, touch) {
+			this.refresherReachMaxAngle = true;
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this.isTouchmoving = true;
+			this.isTouchEnded = false;
+			// 更新下拉刷新状态
+			// 下拉刷新距离超过阈值
+			if (moveDis >= this.finalRefresherThreshold) {
+				// 如果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
+				this.refresherStatus = this.refresherF2Enabled && moveDis >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
+			} else {
+				// 下拉刷新距离未超过阈值,显示默认状态
+				this.refresherStatus = Enum.Refresher.Default;
+			}
+			// #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
+			// this.scrollEnable = false;
+			// 通过transform控制下拉刷新view垂直偏移
+			this.refresherTransform = `translateY(${moveDis}px)`;
+			this.lastRefresherTouchmove = touch;
+			// #endif
+			this.moveDis = moveDis;
+		},
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		// touch结束
+		_refresherTouchend(e) {
+			// 下拉刷新用户手离开屏幕,允许列表滚动
+			this._handleScrollViewBounce({bounce: true});
+			if (this._touchDisabled() || !this.isTouchmoving) return;
+			const touch = u.getTouch(e);
+			let refresherTouchendY = touch.touchY;
+			let moveDis = refresherTouchendY - this.refresherTouchstartY;
+			moveDis = this._getFinalRefresherMoveDis(moveDis);
+			this._handleRefresherTouchend(moveDis);
+			this.disabledBounce = false;
+		},
+		// #endif
+		// 进一步处理touch结束结果
+		_handleRefresherTouchend(moveDis) {
+			// #ifndef APP-PLUS || H5 || MP-WEIXIN
+			if (!this.isTouchmoving) return;
+			// #endif
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this.refresherReachMaxAngle = true;
+			this.isTouchEnded = true;
+			const refresherThreshold = this.finalRefresherThreshold;
+			if (moveDis >= refresherThreshold && [Enum.Refresher.ReleaseToRefresh, Enum.Refresher.GoF2].indexOf(this.refresherStatus) >= 0) {
+				// 如果是松手进入二楼状态,则触发进入二楼
+				if (this.refresherStatus === Enum.Refresher.GoF2) {
+					this._handleGoF2();
+					this._refresherEnd();
+				} else {
+					// 如果是松手立即刷新状态,则触发下拉刷新
+					// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+					this.refresherTransform = `translateY(${refresherThreshold}px)`;
+					this.refresherTransition = 'transform .1s linear';
+					// #endif
+					u.delay(() => {
+						this._emitTouchmove({ pullingDistance: refresherThreshold, dy: this.moveDis - refresherThreshold });
+					}, 0.1);
+					this.moveDis = refresherThreshold;
+					this.refresherStatus = Enum.Refresher.Loading;
+					this._doRefresherLoad();
+				}
+			} else {
+				this._refresherEnd();
+				this.isTouchmovingTimeout = u.delay(() => {
+					this.isTouchmoving = false;
+				}, this.refresherDefaultDuration);
+			}
+			this.scrollEnable = true;
+			this.$emit('refresherTouchend', moveDis);
+		},
+		// 处理列表触摸开始事件
+		_handleListTouchstart() {
+			if (this.useChatRecordMode && this.autoHideKeyboardWhenChat) {
+				uni.hideKeyboard();
+				this.$emit('hidedKeyboard');
+			}
+		},
+		// 处理scroll-view bounce是否生效
+		_handleScrollViewBounce({ bounce }) {
+			if (!this.usePageScroll && !this.scrollToTopBounceEnabled) {
+				if (this.wxsScrollTop <= 5) {
+					// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
+					this.refresherTransition = '';
+					// #endif
+					this.scrollEnable = bounce;
+				} else if (bounce) {
+					this.scrollEnable = bounce;
+				}
+			}
+		},
+		// wxs正在下拉状态改变处理
+		_handleWxsPullingDownStatusChange(onPullingDown) {
+			this.wxsOnPullingDown = onPullingDown;
+			if (onPullingDown && !this.useChatRecordMode) {
+				this.renderPropScrollTop = 0;
+			}
+		},
+		// wxs正在下拉处理
+		_handleWxsPullingDown({ moveDis, diffDis }){
+			this._emitTouchmove({ pullingDistance: moveDis,dy: diffDis });
+		},
+		// wxs触摸方向改变
+		_handleTouchDirectionChange({ direction }) {
+			this.$emit('touchDirectionChange',direction);
+		},
+		// wxs通知更新其props
+		_handlePropUpdate(){
+			this.wxsPropType = u.getTime().toString();
+		},
+		// 下拉刷新结束
+		_refresherEnd(shouldEndLoadingDelay = true, fromAddData = false, isUserPullDown = false, setLoading = true) {
+			if (this.loadingType === Enum.LoadingType.Refresher) {
+				// 计算当前下拉刷新结束需要延迟的时间
+				const refresherCompleteDelay = (fromAddData && (isUserPullDown || this.showRefresherWhenReload)) ? this.refresherCompleteDelay : 0;
+				// 如果延迟时间大于0,则展示刷新结束状态,否则直接展示默认状态
+				const refresherStatus = refresherCompleteDelay > 0 ? Enum.Refresher.Complete : Enum.Refresher.Default;
+				if (this.finalShowRefresherWhenReload) {
+					const stackCount = this.refresherRevealStackCount;
+					this.refresherRevealStackCount --;
+					if (stackCount > 1) return;
+				}
+				this._cleanRefresherEndTimeout();
+				this.refresherEndTimeout = u.delay(() => {
+					// 更新下拉刷新状态
+					this.refresherStatus = refresherStatus;
+					// 如果当前下拉刷新状态不是刷新结束,则认为其不在刷新结束状态
+					if (refresherStatus !== Enum.Refresher.Complete) {
+						this.isRefresherInComplete = false;
+					}
+				}, this.refresherStatus !== Enum.Refresher.Default && refresherStatus === Enum.Refresher.Default ? this.refresherCompleteDuration : 0);
+				
+				// #ifndef APP-NVUE
+				if (refresherCompleteDelay > 0) {
+					this.isRefresherInComplete = true;
+				}
+				// #endif
+				this._cleanRefresherCompleteTimeout();
+				this.refresherCompleteTimeout = u.delay(() => {
+					let animateDuration = 1;
+					const animateType = this.refresherEndBounceEnabled && fromAddData ? 'cubic-bezier(0.19,1.64,0.42,0.72)' : 'linear';
+					if (fromAddData) {
+						animateDuration = this.refresherEndBounceEnabled ? this.refresherCompleteDuration / 1000 : this.refresherCompleteDuration / 3000;
+					}
+					this.refresherTransition = `transform ${fromAddData ? animateDuration : this.refresherDefaultDuration / 1000}s ${animateType}`;
+					// #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
+					this.refresherTransform = 'translateY(0px)';
+					this.currentDis = 0;
+					// #endif
+					// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
+					this.wxsPropType = this.refresherTransition + 'end' + u.getTime();
+					// #endif
+					// #ifdef APP-NVUE
+					this._nRefresherEnd();
+					// #endif
+					this.moveDis = 0;
+					// #ifndef APP-NVUE
+					if (refresherStatus === Enum.Refresher.Complete) {
+						if (this.refresherCompleteSubTimeout) {
+							clearTimeout(this.refresherCompleteSubTimeout);
+							this.refresherCompleteSubTimeout = null;
+						}
+						this.refresherCompleteSubTimeout = u.delay(() => {
+							this.$nextTick(() => {
+								this.refresherStatus = Enum.Refresher.Default;
+								this.isRefresherInComplete = false;
+							})
+						}, animateDuration * 800);
+					}
+					// #endif
+					this._emitTouchmove({ pullingDistance: 0, dy: this.moveDis });
+				}, refresherCompleteDelay);
+			}
+			if (setLoading) {
+				u.delay(() => this.loading = false, shouldEndLoadingDelay ? 10 : 0);
+				isUserPullDown && this._onRestore();
+			}
+		},
+		// 处理进入二楼
+		_handleGoF2() {
+			if (this.showF2 || !this.refresherF2Enabled) return;
+			this.$emit('refresherF2Change', 'go');
+			
+			if (!this.showRefresherF2) return;
+			// #ifndef APP-NVUE
+			this.f2Transform = `translateY(${-this.superContentHeight}px)`;
+			this.showF2 = true;
+			u.delay(() => {
+				this.f2Transform = 'translateY(0px)';
+			}, 100, 'f2ShowDelay')
+			// #endif
+			
+			// #ifdef APP-NVUE
+			this.showF2 = true;
+			this.$nextTick(() => {
+				weexAnimation.transition(this.$refs['zp-n-f2'], {
+					styles: { transform: `translateY(${-this.superContentHeight}px)` },
+					duration: 0,
+					timingFunction: 'linear',
+					needLayout: true,
+					delay: 0
+				})
+				this.nF2Opacity = 1;
+			})
+			u.delay(() => {
+				weexAnimation.transition(this.$refs['zp-n-f2'], {
+					styles: { transform: 'translateY(0px)' },
+					duration: this.refresherF2Duration,
+					timingFunction: 'linear',
+					needLayout: true,
+					delay: 0
+				})
+			}, 10, 'f2GoDelay')
+			// #endif
+		},
+		// 处理退出二楼
+		_handleCloseF2() {
+			if (!this.showF2 || !this.refresherF2Enabled) return;
+			this.$emit('refresherF2Change', 'close');
+			
+			if (!this.showRefresherF2) return;
+			// #ifndef APP-NVUE
+			this.f2Transform = `translateY(${-this.superContentHeight}px)`;
+			// #endif
+			
+			// #ifdef APP-NVUE
+			weexAnimation.transition(this.$refs['zp-n-f2'], {
+				styles: { transform: `translateY(${-this.superContentHeight}px)` },
+				duration: this.refresherF2Duration,
+				timingFunction: 'linear',
+				needLayout: true,
+				delay: 0
+			})
+			// #endif
+			
+			u.delay(() => {
+				this.showF2 = false;
+				this.nF2Opacity = 0;
+			}, this.refresherF2Duration, 'f2CloseDelay')
+		},
+		// 模拟用户手动触发下拉刷新
+		_doRefresherRefreshAnimate() {
+			this._cleanRefresherCompleteTimeout();
+			// 用户处理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
+			// #ifndef APP-NVUE
+			const doRefreshAnimateAfter = !this.doRefreshAnimateAfter && (this.finalShowRefresherWhenReload) && this
+				.customRefresherHeight === -1 && this.refresherThreshold === u.addUnit(80, this.unit);
+			if (doRefreshAnimateAfter) {
+				this.doRefreshAnimateAfter = true;
+				return;
+			}
+			// #endif
+			this.refresherRevealStackCount ++;
+			// #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5
+			this.refresherTransform = `translateY(${this.finalRefresherThreshold}px)`;
+			// #endif
+			// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
+			this.wxsPropType = 'begin' + u.getTime();
+			// #endif
+			this.moveDis = this.finalRefresherThreshold;
+			this.refresherStatus = Enum.Refresher.Loading;
+			this.isTouchmoving = true;
+			this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
+			this._doRefresherLoad(false);
+		},
+		// 触发下拉刷新
+		_doRefresherLoad(isUserPullDown = true) {
+			this._onRefresh(false, isUserPullDown);
+			this.loading = true;
+		},
+		// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
+		// 获取处理后的moveDis
+		_getFinalRefresherMoveDis(moveDis) {
+			let diffDis = moveDis - this.oldCurrentMoveDis;
+			this.oldCurrentMoveDis = moveDis;
+			if (diffDis > 0) {
+				// 根据配置的下拉刷新用户手势位移与实际需要的位移比率计算最终的diffDis
+				diffDis = diffDis * this.finalRefresherPullRate;
+				if (this.currentDis > this.finalRefresherThreshold) {
+					diffDis = diffDis * (1 - this.finalRefresherOutRate);
+				}
+			}
+			// 控制diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
+			diffDis = diffDis > 100 ? diffDis / 100 : diffDis;
+			this.currentDis += diffDis;
+			this.currentDis = Math.max(0, this.currentDis);
+			return this.currentDis;
+		},
+		// 判断touch手势是否要触发
+		_touchDisabled() {
+			const checkOldScrollTop = this.oldScrollTop > 5;
+			return this.loading || this.isRefresherInComplete || this.useChatRecordMode || this.layoutOnly || !this.refresherEnabled || !this.useCustomRefresher || (this.usePageScroll && this.useCustomRefresher && this.pageScrollTop > 10) || (!(this.usePageScroll && this.useCustomRefresher) && checkOldScrollTop);
+		},
+		// #endif
+		// 更新自定义下拉刷新view高度
+		_updateCustomRefresherHeight() {
+			this._getNodeClientRect('.zp-custom-refresher-slot-view').then((res) => {
+				this.customRefresherHeight = res ? res[0].height : 0;
+				this.showCustomRefresher = this.customRefresherHeight > 0;
+				if (this.doRefreshAnimateAfter) {
+					this.doRefreshAnimateAfter = false;
+					this._doRefresherRefreshAnimate();
+				}
+			});
+		},
+		// emit pullingDown事件
+		_emitTouchmove(e) {
+			// #ifndef APP-NVUE
+			e.viewHeight = this.finalRefresherThreshold;
+			// #endif
+			e.rate = e.viewHeight > 0 ? e.pullingDistance / e.viewHeight : 0;
+			this.hasTouchmove && this.oldPullingDistance !== e.pullingDistance && this.$emit('refresherTouchmove', e);
+			this.oldPullingDistance = e.pullingDistance;
+		},
+		// 清除refresherCompleteTimeout
+		_cleanRefresherCompleteTimeout() {
+			this.refresherCompleteTimeout = this._cleanTimeout(this.refresherCompleteTimeout);
+			// #ifdef APP-NVUE
+			this._nRefresherEnd(false);
+			// #endif
+		},
+		// 清除refresherEndTimeout
+		_cleanRefresherEndTimeout() {
+			this.refresherEndTimeout = this._cleanTimeout(this.refresherEndTimeout);
+		},
+	}
+}

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

@@ -0,0 +1,589 @@
+// [z-paging]scroll相关模块
+import u from '.././z-paging-utils'
+import Enum from '.././z-paging-enum'
+
+// #ifdef APP-NVUE
+const weexDom = weex.requireModule('dom');
+// #endif
+
+export default {
+	props: {
+		// 使用页面滚动,默认为否,当设置为是时则使用页面的滚动而非此组件内部的scroll-view的滚动,使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高,但配置会略微繁琐
+		usePageScroll: {
+			type: Boolean,
+			default: u.gc('usePageScroll', false)
+		},
+		// 是否可以滚动,使用内置scroll-view和nvue时有效,默认为是
+		scrollable: {
+			type: Boolean,
+			default: u.gc('scrollable', true)
+		},
+		// 控制是否出现滚动条,默认为是
+		showScrollbar: {
+			type: Boolean,
+			default: u.gc('showScrollbar', true)
+		},
+		// 是否允许横向滚动,默认为否
+		scrollX: {
+			type: Boolean,
+			default: u.gc('scrollX', false)
+		},
+		// iOS设备上滚动到顶部时是否允许回弹效果,默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯,但是有吸顶view时滚动到顶部时可能出现抖动。
+		scrollToTopBounceEnabled: {
+			type: Boolean,
+			default: u.gc('scrollToTopBounceEnabled', false)
+		},
+		// iOS设备上滚动到底部时是否允许回弹效果,默认为是。
+		scrollToBottomBounceEnabled: {
+			type: Boolean,
+			default: u.gc('scrollToBottomBounceEnabled', true)
+		},
+		// 在设置滚动条位置时使用动画过渡,默认为否
+		scrollWithAnimation: {
+			type: Boolean,
+			default: u.gc('scrollWithAnimation', false)
+		},
+		// 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
+		scrollIntoView: {
+			type: String,
+			default: u.gc('scrollIntoView', '')
+		},
+	},
+	data() {
+		return {
+			scrollTop: 0,
+			oldScrollTop: 0,
+			scrollLeft: 0,
+			oldScrollLeft: 0,
+			scrollViewStyle: {},
+			scrollViewContainerStyle: {},
+			scrollViewInStyle: {},
+			pageScrollTop: -1,
+			scrollEnable: true,
+			privateScrollWithAnimation: -1,
+			cacheScrollNodeHeight: -1,
+			superContentHeight: 0,
+			lastScrollHeight: 0,
+			lastScrollDirection: '',
+			setContentHeightPending: false
+		}
+	},
+	watch: {
+		oldScrollTop(newVal) {
+			!this.usePageScroll && this._scrollTopChange(newVal,false);
+		},
+		pageScrollTop(newVal) {
+			this.usePageScroll && this._scrollTopChange(newVal,true);
+		},
+		usePageScroll: {
+			handler(newVal) {
+				this.loaded && this.autoHeight && this._setAutoHeight(!newVal);
+				// #ifdef H5
+				if (newVal) {
+					this.$nextTick(() => {
+						const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main;
+						if (mainScrollRef) {
+							mainScrollRef.style = {};
+						}
+					})
+				}
+				// #endif
+			},
+			immediate: true
+		},
+		finalScrollTop(newVal) {
+			this.renderPropScrollTop = newVal < 6 ? 0 : 10;
+		}
+	},
+	computed: {
+		finalScrollWithAnimation() {
+			if (this.privateScrollWithAnimation !== -1) {
+				return this.privateScrollWithAnimation === 1;
+			}
+			return this.scrollWithAnimation;
+		},
+		finalScrollViewStyle() {
+			if (this.superContentZIndex != 1) {
+				this.scrollViewStyle['z-index'] = this.superContentZIndex;
+				this.scrollViewStyle['position'] = 'relative';
+			}
+			return this.scrollViewStyle;
+		},
+		finalScrollTop() {
+			return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop;
+		},
+		// 当前是否是旧版webview
+		finalIsOldWebView() {
+			return this.isOldWebView && !this.usePageScroll;
+		},
+		// 当前scroll-view/list-view是否允许滚动
+		finalScrollable() {
+			return this.scrollable && !this.usePageScroll && this.scrollEnable 
+			&& (this.refresherCompleteScrollable ? true : this.refresherStatus !== Enum.Refresher.Complete)
+			&& (this.refresherRefreshingScrollable ? true : this.refresherStatus !== Enum.Refresher.Loading);
+		}
+	},
+	methods: {
+		// 滚动到顶部,animate为是否展示滚动动画,默认为是
+		scrollToTop(animate, checkReverse = true) {
+			// 如果是聊天记录模式并且列表倒置了,则滚动到顶部实际上是滚动到底部
+			if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
+				this.scrollToBottom(animate, false);
+				return;
+			}
+			this.$nextTick(() => {
+				this._scrollToTop(animate, false);
+				// #ifdef APP-NVUE
+				if (this.nvueFastScroll && animate) {
+					u.delay(() => {
+						this._scrollToTop(false, false);
+					});
+				}
+				// #endif
+			})
+		},
+		// 滚动到底部,animate为是否展示滚动动画,默认为是
+		scrollToBottom(animate, checkReverse = true) {
+			// 如果是聊天记录模式并且列表倒置了,则滚动到底部实际上是滚动到顶部
+			if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
+				this.scrollToTop(animate, false);
+				return;
+			}
+			this.$nextTick(() => {
+				this._scrollToBottom(animate);
+				// #ifdef APP-NVUE
+				if (this.nvueFastScroll && animate) {
+					u.delay(() => {
+						this._scrollToBottom(false);
+					});
+				}
+				// #endif
+			})
+		},
+		// 滚动到指定view(vue中有效)。sel为需要滚动的view的id值,不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollIntoViewById(sel, offset, animate) {
+			this._scrollIntoView(sel, offset, animate);
+		},
+		// 滚动到指定view(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollIntoViewByNodeTop(nodeTop, offset, animate) {
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
+			})
+		},
+		// y轴滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollToY(y, offset, animate) {
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this._scrollToY(y, offset, animate);
+			})
+		},
+		// x轴滚动到指定位置(非页面滚动且在vue中有效)。x为与左侧的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollToX(x, offset, animate) {
+			this.scrollLeft = this.oldScrollLeft;
+			this.$nextTick(() => {
+				this._scrollToX(x, offset, animate);
+			})
+		},
+		// 滚动到指定view(nvue中和虚拟列表中有效)。index为需要滚动的view的index(第几个,从0开始);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollIntoViewByIndex(index, offset, animate) {
+			if (index >= this.realTotalData.length) {
+				u.consoleErr('当前滚动的index超出已渲染列表长度,请先通过refreshToPage加载到对应index页并等待渲染成功后再调用此方法!');
+				return;
+			}
+			this.$nextTick(() => {
+				// #ifdef APP-NVUE
+				// 在nvue中,根据index获取对应节点信息并滚动到此节点位置
+				this._scrollIntoView(index, offset, animate);
+				// #endif
+				// #ifndef APP-NVUE
+				if (this.finalUseVirtualList) {
+					const isCellFixed = this.cellHeightMode === Enum.CellHeightMode.Fixed;
+					u.delay(() => {
+						if (this.finalUseVirtualList) {
+							// 虚拟列表 + 每个cell高度完全相同模式下,此时滚动到对应index的cell就是滚动到scrollTop = cellHeight * index的位置
+							// 虚拟列表 + 高度是动态非固定的模式下,此时滚动到对应index的cell就是滚动到scrollTop = 缓存的cell高度数组中第index个的lastTotalHeight的位置
+							const scrollTop = isCellFixed ? this.virtualCellHeight * index : this.virtualHeightCacheList[index].lastTotalHeight;
+							this.scrollToY(scrollTop, offset, animate);
+						}
+					}, isCellFixed ? 0 : 100)
+				}
+				// #endif
+			})
+		},
+		// 滚动到指定view(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
+		scrollIntoViewByView(view, offset, animate) {
+			this._scrollIntoView(view, offset, animate);
+		},
+		// 当使用页面滚动并且自定义下拉刷新时,请在页面的onPageScroll中调用此方法,告知z-paging当前的pageScrollTop,否则会导致在任意位置都可以下拉刷新
+		updatePageScrollTop(value) {
+			this.pageScrollTop = value;
+		},
+		// 当使用页面滚动并且设置了slot="top"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="top"的view高度动态改变时,在其高度需要更新时调用此方法
+		updatePageScrollTopHeight() {
+			this._updatePageScrollTopOrBottomHeight('top');
+		},
+		// 当使用页面滚动并且设置了slot="bottom"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="bottom"的view高度动态改变时,在其高度需要更新时调用此方法
+		updatePageScrollBottomHeight() {
+			this._updatePageScrollTopOrBottomHeight('bottom');
+		},
+		// 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
+		updateLeftAndRightWidth() {
+			if (!this.finalIsOldWebView) return;
+			this.$nextTick(() => this._updateLeftAndRightWidth(this.scrollViewContainerStyle, 'zp-page'));
+		},
+		// 更新z-paging内置scroll-view的scrollTop
+		updateScrollViewScrollTop(scrollTop, animate = true) {
+			this._updatePrivateScrollWithAnimation(animate);
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this.scrollTop = scrollTop;
+				this.oldScrollTop = this.scrollTop;
+			});
+		},
+		
+		// 当滚动到顶部时
+		_onScrollToUpper() {
+			this._emitScrollEvent('scrolltoupper');
+			this.$emit('scrollTopChange', 0);
+			this.$nextTick(() => {
+				this.oldScrollTop = 0;
+			})
+		},
+		// 当滚动到底部时
+		_onScrollToLower(e) {
+			(!e.detail || !e.detail.direction || e.detail.direction === 'bottom') 
+			&& this.toBottomLoadingMoreEnabled
+			&& this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom');
+		},
+		// 滚动到顶部
+		_scrollToTop(animate = true, isPrivate = true) {
+			// #ifdef APP-NVUE
+			// 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在顶部插入了一个view,使得滚动到这个view位置
+			const el = this.$refs['zp-n-list-top-tag'];
+			if (this.usePageScroll) {
+				this._getNodeClientRect('zp-page-scroll-top', false).then(node => {
+					const nodeHeight = node ? node[0].height : 0;
+					weexDom.scrollToElement(el, {
+						offset: -nodeHeight,
+						animated: animate
+					});
+				});
+			} else {
+				if (!this.isIos && this.nvueListIs === 'scroller') {
+					this._getNodeClientRect('zp-n-refresh-container', false).then(node => {
+						const nodeHeight = node ? node[0].height : 0;
+						weexDom.scrollToElement(el, {
+							offset: -nodeHeight,
+							animated: animate
+						});
+					});
+				} else {
+					weexDom.scrollToElement(el, {
+						offset: 0,
+						animated: animate
+					});
+				}
+			}
+			return;
+			// #endif
+			if (this.usePageScroll) {
+				this.$nextTick(() => {
+					uni.pageScrollTo({
+						scrollTop: 0,
+						duration: animate ? 100 : 0,
+					});
+				});
+				return;
+			}
+			this._updatePrivateScrollWithAnimation(animate);
+			this.scrollTop = this.oldScrollTop;
+			this.$nextTick(() => {
+				this.scrollTop = 0;
+				this.oldScrollTop = this.scrollTop;
+			});
+		},
+		// 滚动到底部
+		async _scrollToBottom(animate = true) {
+			// #ifdef APP-NVUE
+			// 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在底部插入了一个view,使得滚动到这个view位置
+			const el = this.$refs['zp-n-list-bottom-tag'];
+			if (el) {
+				weexDom.scrollToElement(el, {
+					offset: 0,
+					animated: animate
+				});
+			} else {
+				u.consoleErr('滚动到底部失败,因为您设置了hideNvueBottomTag为true');
+			}
+			return;
+			// #endif
+			if (this.usePageScroll) {
+				this.$nextTick(() => {
+					uni.pageScrollTo({
+						scrollTop: Number.MAX_VALUE,
+						duration: animate ? 100 : 0,
+					});
+				});
+				return;
+			}
+			try {
+				this._updatePrivateScrollWithAnimation(animate);
+				const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container');
+				const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
+				const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
+				const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
+				if (pagingContainerH > scrollViewH) {
+					this.scrollTop = this.oldScrollTop;
+					this.$nextTick(() => {
+						this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight;
+						this.oldScrollTop = this.scrollTop;
+					});
+				}
+			} catch (e) {}
+		},
+		// 滚动到指定view
+		_scrollIntoView(sel, offset = 0, animate = false, finishCallback) {
+			try {
+				this.scrollTop = this.oldScrollTop;
+				this.$nextTick(() => {
+					// #ifdef APP-NVUE
+					const refs = this.$parent.$refs;
+					if (!refs) return;
+					const dataType = Object.prototype.toString.call(sel);
+					let el = null;
+					if (dataType === '[object Number]') {
+						const els = refs[`z-paging-${sel}`];
+						el = els ? els[0] : null;
+					} else if (dataType === '[object Array]') {
+						el = sel[0];
+					} else {
+						el = sel;
+					}
+					if (el) {
+						weexDom.scrollToElement(el, {
+							offset: -offset,
+							animated: animate
+						});
+					} else {
+						u.consoleErr('在nvue中滚动到指定位置,cell必须设置 :ref="`z-paging-${index}`"');
+					}
+					return;
+					// #endif
+					// 获取指定view的节点信息
+					this._getNodeClientRect('#' + sel.replace('#', ''), false).then((node) => {
+						if (node) {
+							// 获取zp-scroll-view-container的节点信息
+							this._getNodeClientRect('.zp-scroll-view-container').then((svContainerNode) => {
+								if (svContainerNode) {
+									// 滚动的top为指定view的top减zp-scroll-view-container的top,因为指定view的top是相对于整个窗口的,需要考虑相对的位置关系
+									this._scrollIntoViewByNodeTop(node[0].top - svContainerNode[0].top, offset, animate);
+									finishCallback && finishCallback();
+								}
+							});
+						} else {
+							u.consoleErr(`无法获取${sel}的节点信息,请检查!`);
+						}
+					});
+				});
+			} catch (e) {}
+		},
+		// 通过nodeTop滚动到指定view
+		_scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
+			// 如果是聊天记录模式并且列表倒置了,此时nodeTop需要等于scroll-view高度 - nodeTop
+			if (this.isChatRecordModeAndInversion) {
+				this._getNodeClientRect('.zp-scroll-view').then(sNode => {
+					if (sNode) {
+						this._scrollToY(sNode[0].height - nodeTop, offset, animate, true);
+					}
+				})
+			} else {
+				this._scrollToY(nodeTop, offset, animate, true);
+			}
+		},
+		// y轴滚动到指定位置
+		_scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
+			this._updatePrivateScrollWithAnimation(animate);
+			u.delay(() => {
+				if (this.usePageScroll) {
+					if (addScrollTop && this.pageScrollTop !== -1) {
+					   y += this.pageScrollTop; 
+					}
+					const scrollTop = y - offset;
+					uni.pageScrollTo({
+						scrollTop,
+						duration: animate ? 100 : 0
+					});
+				} else {
+					if (addScrollTop) {
+					   y += this.oldScrollTop; 
+					}
+					this.scrollTop = y - offset;
+				}
+			}, 10)
+		},
+		// x轴滚动到指定位置
+		_scrollToX(x, offset = 0, animate = false) {
+			this._updatePrivateScrollWithAnimation(animate);
+			u.delay(() => {
+				if (!this.usePageScroll) {
+					this.scrollLeft = x - offset;
+				} else {
+					u.consoleErr('使用页面滚动时不支持scrollToX');
+				}
+			}, 10)
+		},
+		// scroll-view滚动中
+		_scroll(e) {
+			this.$emit('scroll', e);
+			const { scrollTop, scrollLeft, scrollHeight } = e.detail;
+			
+			if (this.watchScrollDirectionChange) {
+				// 计算scroll-view滚动方向,正常情况下上次滚动的oldScrollTop大于当前scrollTop即为向上滚动,反之为向下滚动
+				let direction = this.oldScrollTop > scrollTop ? 'top' : 'bottom';
+				// 此处为解决在iOS中,滚动到顶部因bounce的影响回弹导致滚动方向为bottom的问题:如果滚动到顶部了并且scrollTop小于顶部滚动区域,则强制设置direction为top
+				// 此外发现在h5中下拉刷新时direction有概率被判断为bottom(oldScrollTop > scrollTop),因为下拉刷新时会禁止scroll-view滚动,则以此为依据强制设置direction为top
+				if (scrollTop <= 0 || !this.scrollEnable) {
+					direction = 'top';
+				}
+				// 此处为解决在iOS中,滚动到底部因bounce的影响回弹导致滚动方向为top的问题:如果滚动到底部了并且scrollTop超过底部滚动区域,则强制设置direction为bottom
+				if (scrollTop > this.lastScrollHeight - this.scrollViewHeight - 1 && this.scrollEnable) {
+					direction = 'bottom';
+				}
+				// emit 列表滚动方向改变事件
+				if (direction !== this.lastScrollDirection) {
+					this.$emit('scrollDirectionChange', direction);
+					this.lastScrollDirection = direction;
+				}
+				// 当scrollHeight变化时,需要延迟100毫秒设置lastScrollHeight,如果直接根据scrollHeight的话,因为此时数据还未改变,会导致滚动方向从bottom变为top
+				if (this.lastScrollHeight !== scrollHeight && !this.setContentHeightPending) {
+					// 因此处会多次触发,因此加个标识确保在延时期间仅触发一次
+					this.setContentHeightPending = true;
+					u.delay(() => {
+						this.lastScrollHeight = scrollHeight;
+						this.setContentHeightPending = false;
+					})
+				}
+			}
+			
+			// #ifndef APP-NVUE
+			this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
+			// #endif
+			this.oldScrollTop = scrollTop;
+			this.oldScrollLeft = scrollLeft;
+			// 滚动区域内容的总高度 - 当前滚动的scrollTop = 当前滚动区域的顶部与内容底部的距离
+			const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
+			// 在非ios平台滚动中,再次验证一下是否滚动到了底部。因为在一些安卓设备中,有概率滚动到底部不触发@scrolltolower事件,因此添加双重检测逻辑
+			!this.isIos && this._checkScrolledToBottom(scrollDiff);
+		},
+		// emit scrolltolower/scrolltoupper事件
+		_emitScrollEvent(type) {
+			const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower';
+			const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion ? reversedType : type;
+			this.$emit(eventType);
+		},
+		// 更新内置的scroll-view是否启用滚动动画
+		_updatePrivateScrollWithAnimation(animate) {
+			this.privateScrollWithAnimation = animate ? 1 : 0;
+			u.delay(() => this.$nextTick(() => {
+				// 在滚动结束后将滚动动画状态设置回初始状态
+				this.privateScrollWithAnimation = -1;
+			}), 100, 'updateScrollWithAnimationDelay')
+		},
+		// 检测scrollView是否要铺满屏幕
+		_doCheckScrollViewShouldFullHeight(totalData) {
+			if (this.autoFullHeight && this.usePageScroll && this.isTotalChangeFromAddData) {
+				// #ifndef APP-NVUE
+				this.$nextTick(() => {
+					this._checkScrollViewShouldFullHeight((scrollViewNode, pagingContainerNode) => {
+						this._preCheckShowNoMoreInside(totalData, scrollViewNode, pagingContainerNode)
+					});
+				})
+				// #endif
+				// #ifdef APP-NVUE
+				this._preCheckShowNoMoreInside(totalData)
+				// #endif
+			} else {
+				this._preCheckShowNoMoreInside(totalData)
+			} 
+		},
+		// 检测z-paging是否要全屏覆盖(当使用页面滚动并且不满全屏时,默认z-paging需要铺满全屏,避免数据过少时内部的empty-view无法正确展示)
+		async _checkScrollViewShouldFullHeight(callback) {
+			try {
+				const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
+				const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container-content');
+				if (!scrollViewNode || !pagingContainerNode) return;
+				const scrollViewHeight = pagingContainerNode[0].height;
+				const scrollViewTop = scrollViewNode[0].top;
+				if (this.isAddedData && scrollViewHeight + scrollViewTop <= this.windowHeight) {
+					this._setAutoHeight(true, scrollViewNode);
+					callback(scrollViewNode, pagingContainerNode);
+				} else {
+					this._setAutoHeight(false);
+					callback(null, null);
+				}
+			} catch (e) {
+				callback(null, null);
+			}
+		},
+		// 更新缓存中z-paging整个内容容器高度
+		async _updateCachedSuperContentHeight() {
+			const superContentNode = await this._getNodeClientRect('.z-paging-content');
+			if (superContentNode) {
+				this.superContentHeight = superContentNode[0].height;
+			}
+		},
+		// scrollTop改变时触发
+		_scrollTopChange(newVal, isPageScrollTop){
+			this.$emit('scrollTopChange', newVal);
+			this.$emit('update:scrollTop', newVal);
+			this._checkShouldShowBackToTop(newVal);
+			// 之前在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,因此判断scrollTop在105之内都允许下拉刷新,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
+			// const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : (newVal > 105 ? 106 : (newVal > 5 ? 6 : 0));
+			const scrollTop = newVal > 5 ? 6 : 0;
+			if (isPageScrollTop && this.wxsPageScrollTop !== scrollTop) {
+				this.wxsPageScrollTop = scrollTop;
+			} else if (!isPageScrollTop && this.wxsScrollTop !== scrollTop) {
+				this.wxsScrollTop = scrollTop;
+				if (scrollTop > 6) {
+					this.scrollEnable = true;
+				}
+			}
+		},
+		// 更新使用页面滚动时slot="top"或"bottom"插入view的高度
+		_updatePageScrollTopOrBottomHeight(type) {
+			// #ifndef APP-NVUE
+			if (!this.usePageScroll) return;
+			// #endif
+			this._doCheckScrollViewShouldFullHeight(this.realTotalData);
+			const node = `.zp-page-${type}`;
+			const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
+			// 是否设置底部安全区域间距,仅当开启底部安全区域并且slot=bottom不存在的时候才处理,如果slot=bottom存在则直接在bottom底部插入占位view
+			// 如果useSafeAreaPlaceholder为true,这里也不需要额外通过marginBottom设置底部安全区域了
+			const safeAreaInsetBottomAdd = this.safeAreaInsetBottom && !this.zSlots.bottom && !this.useSafeAreaPlaceholder;
+			this.$nextTick(() => {
+				let delayTime = 0;
+				// #ifdef MP-BAIDU || APP-NVUE
+				delayTime = 50;
+				// #endif
+				u.delay(() => {
+					this._getNodeClientRect(node).then((res) => {
+						if (res) {
+							let pageScrollNodeHeight = res[0].height;
+							if (type === 'bottom') {
+								if (safeAreaInsetBottomAdd) {
+									pageScrollNodeHeight += this.safeAreaBottom;
+								}
+							} else {
+								this.cacheTopHeight = pageScrollNodeHeight;
+							}
+							this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`);
+						} else if (safeAreaInsetBottomAdd) {
+							this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`);
+						}
+					});
+				}, delayTime)
+			})
+		},
+	}
+}

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

@@ -0,0 +1,539 @@
+// [z-paging]虚拟列表模块
+import u from '.././z-paging-utils'
+import c from '.././z-paging-constant'
+import Enum from '.././z-paging-enum'
+
+export default {
+	props: {
+		// 是否使用虚拟列表,默认为否
+		useVirtualList: {
+			type: Boolean,
+			default: u.gc('useVirtualList', false)
+		},
+		// 在使用虚拟列表时,是否使用兼容模式,默认为否
+		useCompatibilityMode: {
+			type: Boolean,
+			default: u.gc('useCompatibilityMode', false)
+		},
+		// 使用兼容模式时传递的附加数据
+		extraData: {
+			type: Object,
+			default: u.gc('extraData', {})
+		},
+		// 是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
+		useInnerList: {
+			type: Boolean,
+			default: u.gc('useInnerList', false)
+		},
+		// 强制关闭inner-list,默认为false,如果为true将强制关闭innerList,适用于开启了虚拟列表后需要强制关闭inner-list的情况
+		forceCloseInnerList: {
+			type: Boolean,
+			default: u.gc('forceCloseInnerList', false)
+		},
+		// 内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
+		cellKeyName: {
+			type: String,
+			default: u.gc('cellKeyName', '')
+		},
+		// innerList样式
+		innerListStyle: {
+			type: Object,
+			default: u.gc('innerListStyle', {})
+		},
+		// innerCell样式
+		innerCellStyle: {
+			type: Object,
+			default: u.gc('innerCellStyle', {})
+		},
+		// 预加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
+		preloadPage: {
+			type: [Number, String],
+			default: u.gc('preloadPage', 12),
+			validator: (value) => {
+				if (value <= 0) u.consoleErr('preload-page必须大于0!');
+				return value > 0;
+			}
+		},
+		// 虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
+		cellHeightMode: {
+			type: String,
+			default: u.gc('cellHeightMode', Enum.CellHeightMode.Fixed)
+		},
+		// 固定的cell高度,cellHeightMode=fixed才有效,若设置了值,则不计算第一个cell高度而使用设置的cell高度
+		fixedCellHeight: {
+			type: [Number, String],
+			default: u.gc('fixedCellHeight', 0)
+		},
+		// 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
+		virtualListCol: {
+			type: [Number, String],
+			default: u.gc('virtualListCol', 1)
+		},
+		// 虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
+		virtualScrollFps: {
+			type: [Number, String],
+			default: u.gc('virtualScrollFps', 80)
+		},
+		// 虚拟列表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id,注意:请勿传数字或以数字开头的字符串。如设置为list1,则cell的id应为:list1-zp-id-${item.zp_index}
+		virtualCellIdPrefix: {
+			type: String,
+			default: u.gc('virtualCellIdPrefix', '')
+		},
+		// 虚拟列表是否使用swiper-item包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
+		// 仅vue3+(微信小程序或QQ小程序)+非内置列表写法虚拟列表有效,其他情况此属性设置任何值都无效,所以如果您在swiper-item内使用z-paging的非内置虚拟列表写法,将此属性设置为true即可
+		virtualInSwiperSlot: {
+			type: Boolean,
+			default: false
+		},
+	},
+	data() {
+		return {
+			virtualListKey: u.getInstanceId(),
+			virtualCellHeight: 0,
+			virtualScrollTimeStamp: 0,
+			
+			virtualList: [],
+			virtualPlaceholderTopHeight: 0,
+			virtualPlaceholderBottomHeight: 0,
+			virtualTopRangeIndex: 0,
+			virtualBottomRangeIndex: 0,
+			lastVirtualTopRangeIndex: 0,
+			lastVirtualBottomRangeIndex: 0,
+			virtualItemInsertedCount: 0,
+			
+			virtualHeightCacheList: [],
+			
+			getCellHeightRetryCount: {
+				fixed: 0,
+				dynamic: 0
+			},
+			updateVirtualListFromDataChange: false
+		}
+	},
+	watch: {
+		// 监听总数据的改变,刷新虚拟列表布局
+		realTotalData() {
+			this.updateVirtualListRender();
+		},
+		// 监听虚拟列表渲染数组的改变并emit
+		virtualList(newVal){
+			this.$emit('update:virtualList', newVal);
+			this.$emit('virtualListChange', newVal);
+		},
+		// 监听虚拟列表顶部占位高度改变并emit
+		virtualPlaceholderTopHeight(newVal) {
+			this.$emit('virtualTopHeightChange', newVal);
+		}
+	},
+	computed: {
+		virtualCellIndexKey() {
+			return c.listCellIndexKey;
+		},
+		finalUseVirtualList() {
+			if (this.useVirtualList && this.usePageScroll){
+				u.consoleErr('使用页面滚动时,开启虚拟列表无效!');
+			}
+			return this.useVirtualList && !this.usePageScroll;
+		},
+		finalUseInnerList() {
+			return this.useInnerList || (this.finalUseVirtualList && !this.forceCloseInnerList);
+		},
+		finalCellKeyName() {
+			// #ifdef APP-NVUE
+			if (this.finalUseVirtualList && !this.cellKeyName.length){
+				u.consoleErr('在nvue中开启use-virtual-list必须设置cell-key-name,否则将可能导致列表渲染错误!');
+			}
+			// #endif
+			return this.cellKeyName;
+		},
+		finalVirtualPageHeight(){
+			return this.scrollViewHeight > 0 ? this.scrollViewHeight : this.windowHeight;
+		},
+		finalFixedCellHeight() {
+			return u.convertToPx(this.fixedCellHeight);
+		},
+		fianlVirtualCellIdPrefix() {
+			const prefix = this.virtualCellIdPrefix ? this.virtualCellIdPrefix + '-' : '';
+			return prefix + 'zp-id';
+		},
+		finalPlaceholderTopHeightStyle() {
+			// #ifdef VUE2
+			return { transform: this.virtualPlaceholderTopHeight > 0 ? `translateY(${this.virtualPlaceholderTopHeight}px)` : 'none' };
+			// #endif
+			return {};
+		},
+		virtualRangePageHeight(){
+			return this.finalVirtualPageHeight * this.preloadPage;
+		},
+		virtualScrollDisTimeStamp() {
+			return 1000 / this.virtualScrollFps;
+		}
+	},
+	methods: {
+		// 在使用动态高度虚拟列表时,若在列表数组中需要插入某个item,需要调用此方法;item:需要插入的item,index:插入的cell位置,若index为2,则插入的item在原list的index=1之后,index从0开始
+		doInsertVirtualListItem(item, index) {
+			if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
+			this.realTotalData.splice(index, 0, item);
+			// #ifdef VUE3
+			this.realTotalData = [...this.realTotalData];
+			// #endif
+			this.virtualItemInsertedCount ++;
+			if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
+				item = { item };
+			}
+			const cellIndexKey = this.virtualCellIndexKey;
+			item[cellIndexKey] = `custom-${this.virtualItemInsertedCount}`;
+			item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
+			this.$nextTick(async () => {
+				let retryCount = 0;
+				while (retryCount <= 10) {
+					await u.wait(c.delayTime);
+					
+					const cellNode = await this._getVirtualCellNodeByIndex(item[cellIndexKey]);
+					// 如果获取当前cell的节点信息失败,则重试(不超过10次)
+					if (!cellNode) {
+						retryCount ++;
+						continue;
+					} 
+					
+					const currentHeight = cellNode ? cellNode[0].height : 0;
+					const lastHeightCache = this.virtualHeightCacheList[index - 1];
+					const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
+					// 在缓存的cell高度数组中,插入此cell高度信息
+					this.virtualHeightCacheList.splice(index, 0, {
+						height: currentHeight,
+						lastTotalHeight,
+						totalHeight: lastTotalHeight + currentHeight
+					});
+					
+					// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell的高度
+					for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
+						const thisNode = this.virtualHeightCacheList[i];
+						thisNode.lastTotalHeight += currentHeight;
+						thisNode.totalHeight += currentHeight;
+					}
+					
+					this._updateVirtualScroll(this.oldScrollTop);
+					break;
+				}
+			})
+		},
+		// 在使用动态高度虚拟列表时,手动更新指定cell的缓存高度(当cell高度在初始化之后再次改变后调用);index:需要更新的cell在列表中的位置,从0开始
+		didUpdateVirtualListCell(index) {
+			if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
+			const currentNode = this.virtualHeightCacheList[index];
+			this.$nextTick(() => {
+				this._getVirtualCellNodeByIndex(index).then(cellNode => {
+					// 更新当前cell的高度
+					const cellNodeHeight = cellNode ? cellNode[0].height : 0;
+					const heightDis = cellNodeHeight - currentNode.height;
+					currentNode.height = cellNodeHeight;
+					currentNode.totalHeight = currentNode.lastTotalHeight + cellNodeHeight;
+					
+					// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell变化的高度
+					for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
+						const thisNode = this.virtualHeightCacheList[i];
+						thisNode.totalHeight += heightDis;
+						thisNode.lastTotalHeight += heightDis;
+					}
+				});
+			})
+		},
+		// 在使用动态高度虚拟列表时,若删除了列表数组中的某个item,需要调用此方法以更新高度缓存数组;index:删除的cell在列表中的位置,从0开始
+		didDeleteVirtualListCell(index) {
+			if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
+			const currentNode = this.virtualHeightCacheList[index];
+			// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要减去当前cell的高度
+			for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
+				const thisNode = this.virtualHeightCacheList[i];
+				thisNode.totalHeight -= currentNode.height;
+				thisNode.lastTotalHeight -= currentNode.height;
+			}
+			// 将当前cell的高度信息从高度缓存数组中删除
+			this.virtualHeightCacheList.splice(index, 1);
+		},
+		// 手动触发虚拟列表渲染更新,可用于解决例如修改了虚拟列表数组中元素,但展示未更新的情况
+		updateVirtualListRender() {
+			// #ifndef APP-NVUE
+			if (this.finalUseVirtualList) {
+				this.updateVirtualListFromDataChange = true;
+				this.$nextTick(() => {
+					this.getCellHeightRetryCount.fixed = 0;
+					if (this.realTotalData.length) {
+						this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight()
+					} else {
+						this._resetDynamicListState(!this.isUserPullDown);
+					}
+					this._updateVirtualScroll(this.oldScrollTop);
+				})
+			}
+			// #endif
+		},
+		// cellHeightMode为fixed时获取第一个cell高度
+		_updateFixedCellHeight() {
+			if (!this.finalFixedCellHeight) {
+				this.$nextTick(() => {
+					u.delay(() => {
+						this._getVirtualCellNodeByIndex(0).then(cellNode => {
+							if (!cellNode) {
+								if (this.getCellHeightRetryCount.fixed > 10) return;
+								this.getCellHeightRetryCount.fixed ++;
+								// 如果获取第一个cell的节点信息失败,则重试(不超过10次)
+								this._updateFixedCellHeight();
+							} else {
+								this.virtualCellHeight = cellNode[0].height;
+								this._updateVirtualScroll(this.oldScrollTop);
+							}
+						});
+					}, c.delayTime, 'updateFixedCellHeightDelay');
+				})
+			} else {
+				this.virtualCellHeight = this.finalFixedCellHeight;
+			}
+		},
+		// cellHeightMode为dynamic时获取每个cell高度
+		_updateDynamicCellHeight(list, dataFrom = 'bottom') {
+			const dataFromTop = dataFrom === 'top';
+			const heightCacheList = this.virtualHeightCacheList;
+			const currentCacheList = dataFromTop ?  [] : heightCacheList;
+			let listTotalHeight = 0;
+			this.$nextTick(() => {
+				u.delay(async () => {
+					for (let i = 0; i < list.length; i++) {
+						const cellNode = await this._getVirtualCellNodeByIndex(list[i][this.virtualCellIndexKey]);
+						const currentHeight = cellNode ? cellNode[0].height : 0;
+						if (!cellNode) {
+							if (this.getCellHeightRetryCount.dynamic <= 10) {
+								heightCacheList.splice(heightCacheList.length - i, i);
+								this.getCellHeightRetryCount.dynamic ++;
+								// 如果获取当前cell的节点信息失败,则重试(不超过10次)
+								this._updateDynamicCellHeight(list, dataFrom);
+							}
+							return;
+						} 
+						const lastHeightCache = currentCacheList.length ? currentCacheList.slice(-1)[0] : null;
+						const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
+						// 缓存当前cell的高度信息:height-当前cell高度;lastTotalHeight-前面所有cell的高度总和;totalHeight-包含当前cell的所有高度总和
+						currentCacheList.push({
+							height: currentHeight,
+							lastTotalHeight,
+							totalHeight: lastTotalHeight + currentHeight
+						});
+						if (dataFromTop) {
+							listTotalHeight += currentHeight;
+						}
+					}
+					// 如果数据是从顶部拼接的
+					if (dataFromTop && list.length) {
+						for (let i = 0; i < heightCacheList.length; i++) {
+							// 更新之前所有项的缓存高度,需要加上此次插入的所有cell高度之和(因为是从顶部插入的cell)
+							const heightCacheItem = heightCacheList[i];
+							heightCacheItem.lastTotalHeight += listTotalHeight;
+							heightCacheItem.totalHeight += listTotalHeight;
+						}
+						this.virtualHeightCacheList = currentCacheList.concat(heightCacheList);
+					}
+					this._updateVirtualScroll(this.oldScrollTop);
+				}, c.delayTime, 'updateDynamicCellHeightDelay')
+			})
+		},
+		// 设置cellItem的index
+		_setCellIndex(list, dataFrom = 'bottom') {
+			let currentItemIndex = 0;
+			const cellIndexKey = this.virtualCellIndexKey;
+			dataFrom === 'bottom' && ([Enum.QueryFrom.Refresh, Enum.QueryFrom.Reload].indexOf(this.queryFrom) >= 0) && this._resetDynamicListState();
+			if (this.totalData.length && this.queryFrom !== Enum.QueryFrom.Refresh) {
+				if (dataFrom === 'bottom') {
+					currentItemIndex = this.realTotalData.length;
+					const lastItem = this.realTotalData.length ? this.realTotalData.slice(-1)[0] : null;
+					if (lastItem && lastItem[cellIndexKey] !== undefined) {
+						currentItemIndex = lastItem[cellIndexKey] + 1;
+					}
+				} else if (dataFrom === 'top') {
+					const firstItem = this.realTotalData.length ? this.realTotalData[0] : null;
+					if (firstItem && firstItem[cellIndexKey] !== undefined) {
+						currentItemIndex = firstItem[cellIndexKey] - list.length;
+					}
+				}
+			} else {
+				this._resetDynamicListState();
+			}
+			for (let i = 0; i < list.length; i++) {
+				let item = list[i];
+				if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
+					item = { item };
+				}
+				if (item[c.listCellIndexUniqueKey]) {
+					item = u.deepCopy(item);
+				}
+				item[cellIndexKey] = currentItemIndex + i;
+				item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
+				list[i] = item;
+			}
+			this.getCellHeightRetryCount.dynamic = 0;
+			this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list, dataFrom);
+		},
+		// 更新scroll滚动(虚拟列表滚动时触发)
+		_updateVirtualScroll(scrollTop, scrollDiff = 0) {
+			const currentTimeStamp = u.getTime();
+			scrollTop === 0 && this._resetTopRange();
+			if (scrollTop !== 0 && this.virtualScrollTimeStamp && currentTimeStamp - this.virtualScrollTimeStamp <= this.virtualScrollDisTimeStamp) {
+				return;
+			}
+			this.virtualScrollTimeStamp = currentTimeStamp;
+			
+			let scrollIndex = 0;
+			const cellHeightMode = this.cellHeightMode;
+			if (cellHeightMode === Enum.CellHeightMode.Fixed) {
+				// 如果是固定高度的虚拟列表
+				// 计算当前滚动到的cell的index = scrollTop / 虚拟列表cell的固定高度
+				scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
+				// 更新顶部和底部占位view的高度(为兼容考虑,顶部采用transformY的方式占位)
+				this._updateFixedTopRangeIndex(scrollIndex);
+				this._updateFixedBottomRangeIndex(scrollIndex);
+			} else if(cellHeightMode === Enum.CellHeightMode.Dynamic) {
+				// 如果是不固定高度的虚拟列表
+				// 当前滚动的方向
+				const scrollDirection = scrollDiff > 0 ? 'top' : 'bottom';
+				// 视图区域的高度
+				const rangePageHeight = this.virtualRangePageHeight;
+				// 顶部视图区域外的高度(顶部不需要渲染而是需要占位部分的高度)
+				const topRangePageOffset = scrollTop - rangePageHeight;
+				// 底部视图区域外的高度(底部不需要渲染而是需要占位部分的高度)
+				const bottomRangePageOffset = scrollTop + this.finalVirtualPageHeight + rangePageHeight;
+				
+				let virtualBottomRangeIndex = 0;
+				let virtualPlaceholderBottomHeight = 0;
+				let reachedLimitBottom = false;
+				const heightCacheList = this.virtualHeightCacheList;
+				const lastHeightCache = !!heightCacheList ? heightCacheList.slice(-1)[0] : null;
+				
+				let startTopRangeIndex = this.virtualTopRangeIndex;
+				// 如果是向底部滚动(顶部占位的高度不断增大,顶部的实际渲染cell数量不断减少)
+				if (scrollDirection === 'bottom') {
+					// 从顶部视图边缘的cell的位置开始向后查找
+					for (let i = startTopRangeIndex; i < heightCacheList.length; i++){
+						const heightCacheItem = heightCacheList[i];
+						// 如果查找到某个cell对应的totalHeight大于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
+						if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
+							// 记录顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
+							this.virtualTopRangeIndex = i;
+							this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
+							break;
+						}
+					}
+				} else {
+					// 如果是向顶部滚动(顶部占位的高度不断减少,顶部的实际渲染cell数量不断增加)
+					let topRangeMatched = false;
+					// 从顶部视图边缘的cell的位置开始向前查找
+					for (let i = startTopRangeIndex; i >= 0; i--){
+						const heightCacheItem = heightCacheList[i];
+						// 如果查找到某个cell对应的totalHeight小于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
+						if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
+							// 记录顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
+							this.virtualTopRangeIndex = i;
+							this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
+							topRangeMatched = true;
+							break;
+						}
+					}
+					// 如果查找不到,则认为顶部占位高度为0了,顶部cell不需要继续复用,重置topRangeIndex和placeholderTopHeight
+					!topRangeMatched && this._resetTopRange();
+				}
+				// 从顶部视图边缘的cell的位置开始向后查找
+				for (let i = this.virtualTopRangeIndex; i < heightCacheList.length; i++){
+					const heightCacheItem = heightCacheList[i];
+					// 如果查找到某个cell对应的totalHeight大于底部视图区域外的高度,则此cell为底部视图边缘的cell
+					if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
+						// 记录底部视图边缘cell的index并更新底部占位区域的高度并停止继续查找
+						virtualBottomRangeIndex = i;
+						virtualPlaceholderBottomHeight = lastHeightCache.totalHeight - heightCacheItem.totalHeight;
+						reachedLimitBottom = true;
+						break;
+					}
+				}
+				if (!reachedLimitBottom || this.virtualBottomRangeIndex === 0) {
+					this.virtualBottomRangeIndex = this.realTotalData.length ? this.realTotalData.length - 1 : this.pageSize;
+					this.virtualPlaceholderBottomHeight = 0;
+				} else {
+					this.virtualBottomRangeIndex = virtualBottomRangeIndex;
+					this.virtualPlaceholderBottomHeight = virtualPlaceholderBottomHeight;
+				}
+				this._updateVirtualList();
+			}
+		},
+		// 更新fixedCell模式下topRangeIndex&placeholderTopHeight
+		_updateFixedTopRangeIndex(scrollIndex) {
+			let virtualTopRangeIndex = this.virtualCellHeight === 0 ? 0 : scrollIndex - (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * this.preloadPage;
+			virtualTopRangeIndex *= this.virtualListCol;
+			virtualTopRangeIndex = Math.max(0, virtualTopRangeIndex);
+			this.virtualTopRangeIndex = virtualTopRangeIndex;
+			this.virtualPlaceholderTopHeight = (virtualTopRangeIndex / this.virtualListCol) * this.virtualCellHeight;
+		},
+		// 更新fixedCell模式下bottomRangeIndex&placeholderBottomHeight
+		_updateFixedBottomRangeIndex(scrollIndex) {
+			let virtualBottomRangeIndex = this.virtualCellHeight === 0 ? this.pageSize : scrollIndex + (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * (this.preloadPage + 1);
+			virtualBottomRangeIndex *= this.virtualListCol;
+			virtualBottomRangeIndex = Math.min(this.realTotalData.length, virtualBottomRangeIndex);
+			this.virtualBottomRangeIndex = virtualBottomRangeIndex;
+			this.virtualPlaceholderBottomHeight = (this.realTotalData.length - virtualBottomRangeIndex) * this.virtualCellHeight / this.virtualListCol;
+			this._updateVirtualList();
+		},
+		// 更新virtualList
+		_updateVirtualList() {
+			const shouldUpdateList = this.updateVirtualListFromDataChange || (this.lastVirtualTopRangeIndex !== this.virtualTopRangeIndex || this.lastVirtualBottomRangeIndex !== this.virtualBottomRangeIndex);
+			if (shouldUpdateList) {
+				this.updateVirtualListFromDataChange = false;
+				this.lastVirtualTopRangeIndex =  this.virtualTopRangeIndex;
+				this.lastVirtualBottomRangeIndex = this.virtualBottomRangeIndex;
+				this.virtualList = this.realTotalData.slice(this.virtualTopRangeIndex, this.virtualBottomRangeIndex + 1);
+			}
+		},
+		// 重置动态cell模式下的高度缓存数据、虚拟列表和滚动状态
+		_resetDynamicListState(resetVirtualList = false) {
+			this.virtualHeightCacheList = [];
+			if (resetVirtualList) {
+				this.virtualList = [];
+			}
+			this.virtualTopRangeIndex = 0;
+			this.virtualPlaceholderTopHeight = 0;
+		},
+		// 重置topRangeIndex和placeholderTopHeight
+		_resetTopRange() {
+			this.virtualTopRangeIndex = 0;
+			this.virtualPlaceholderTopHeight = 0;
+			this._updateVirtualList();
+		},
+		// 检测虚拟列表当前滚动位置,如发现滚动位置不正确则重新计算虚拟列表相关参数(为解决在App中可能出现的长时间进入后台后打开App白屏的问题)
+		_checkVirtualListScroll() {
+			if (this.finalUseVirtualList) {
+				this.$nextTick(() => {
+					this._getNodeClientRect('.zp-paging-touch-view').then(node => {
+						const currentTop = node ? node[0].top : 0;
+						if (!node || (currentTop === this.pagingOrgTop && this.virtualPlaceholderTopHeight !== 0)) {
+							this._updateVirtualScroll(0);
+						}
+					});
+				})
+			}
+		},
+		// 获取对应index的虚拟列表cell节点信息
+		_getVirtualCellNodeByIndex(index) {
+			let inDom = this.finalUseInnerList;
+			// 在vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度的问题
+			// 通过uni.createSelectorQuery().in(this.$parent)来解决此问题
+			// #ifdef VUE3
+			// #ifdef MP-WEIXIN || MP-QQ
+			if (this.forceCloseInnerList && this.virtualInSwiperSlot) {
+				inDom = this.$parent;
+			}
+			// #endif
+			// #endif
+			return this._getNodeClientRect(`#${this.fianlVirtualCellIdPrefix}-${index}`, inDom);
+		},
+		// 处理使用内置列表时点击了cell事件
+		_innerCellClick(item, index) {
+			this.$emit('innerCellClick', item, index);
+		}
+	}
+}

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

@@ -0,0 +1,19 @@
+// [z-paging]常量
+
+export default {
+	// 当前版本号
+	version: '2.8.7',
+	// 延迟操作的通用时间
+	delayTime: 100,
+	// 请求失败时候全局emit使用的key
+	errorUpdateKey: 'z-paging-error-emit',
+	// 全局emit complete的key
+	completeUpdateKey: 'z-paging-complete-emit',
+	// z-paging缓存的前缀key
+	cachePrefixKey: 'z-paging-cache',
+	
+	// 虚拟列表中列表index的key
+	listCellIndexKey: 'zp_index',
+	// 虚拟列表中列表的唯一key
+	listCellIndexUniqueKey: 'zp_unique_index'
+}

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

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

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

@@ -0,0 +1,97 @@
+// [z-paging]拦截器
+
+const queryKey = 'Query';
+const fetchParamsKey = 'FetchParams';
+const fetchResultKey = 'FetchResult';
+const language2LocalKey = 'Language2Local';
+
+// 拦截&处理@query事件
+function handleQuery(callback) {
+	_addHandleByKey(queryKey, callback);
+	return this;
+}
+
+// 拦截&处理@query事件(私有,请勿调用)
+function _handleQuery(pageNo, pageSize, from, lastItem) {
+	const callback = _getHandleByKey(queryKey);
+	return callback ? callback(pageNo, pageSize, from, lastItem) : [pageNo, pageSize, from];
+}
+
+// 拦截&处理:fetch参数
+function handleFetchParams(callback) {
+	_addHandleByKey(fetchParamsKey, callback);
+	return this;
+}
+
+// 拦截&处理:fetch参数(私有,请勿调用)
+function _handleFetchParams(parmas, extraParams) {
+	const callback = _getHandleByKey(fetchParamsKey);
+	return callback ? callback(parmas, extraParams || {}) : { pageNo: parmas.pageNo, pageSize: parmas.pageSize, ...(extraParams || {}) };
+}
+
+// 拦截&处理:fetch结果
+function handleFetchResult(callback) {
+	_addHandleByKey(fetchResultKey, callback);
+	return this;
+}
+
+// 拦截&处理:fetch结果(私有,请勿调用)
+function _handleFetchResult(result, paging, params) {
+	const callback = _getHandleByKey(fetchResultKey);
+	callback && callback(result, paging, params);
+	return callback ? true : false;
+}
+
+// 拦截&处理系统language转i18n local
+function handleLanguage2Local(callback) {
+	_addHandleByKey(language2LocalKey, callback);
+	return this;
+}
+
+// 拦截&处理系统language转i18n local(私有,请勿调用)
+function _handleLanguage2Local(language, local) {
+	const callback = _getHandleByKey(language2LocalKey);
+	return callback ? callback(language, local) : local;
+}
+
+// 获取当前app对象
+function _getApp(){
+	// #ifndef APP-NVUE
+	return getApp();
+	// #endif
+	// #ifdef APP-NVUE
+	return getApp({ allowDefault: true });
+	// #endif
+}
+
+// 是否可以访问globalData
+function _hasGlobalData() {
+	return _getApp() && _getApp().globalData;
+}
+
+// 添加处理函数
+function _addHandleByKey(key, callback) {
+	try {
+		setTimeout(function() {
+			if (_hasGlobalData()) {
+				_getApp().globalData[`zp_handle${key}Callback`] = callback;
+			}
+		}, 1);
+	} catch (_) {}
+}
+
+// 获取处理回调函数
+function _getHandleByKey(key) {
+	return _hasGlobalData() ? _getApp().globalData[`zp_handle${key}Callback`] : null;
+}
+
+export default {
+	handleQuery,
+	_handleQuery,
+	handleFetchParams,
+	_handleFetchParams,
+	handleFetchResult,
+	_handleFetchResult,
+	handleLanguage2Local,
+	_handleLanguage2Local
+};

+ 537 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-main.js

@@ -0,0 +1,537 @@
+// [z-paging]核心js
+
+import zStatic from './z-paging-static'
+import c from './z-paging-constant'
+import u from './z-paging-utils'
+
+import zPagingRefresh from '../components/z-paging-refresh'
+import zPagingLoadMore from '../components/z-paging-load-more'
+import zPagingEmptyView from '../../z-paging-empty-view/z-paging-empty-view'
+
+// modules
+import commonLayoutModule from './modules/common-layout'
+import dataHandleModule from './modules/data-handle'
+import i18nModule from './modules/i18n'
+import nvueModule from './modules/nvue'
+import emptyModule from './modules/empty'
+import refresherModule from './modules/refresher'
+import loadMoreModule from './modules/load-more'
+import loadingModule from './modules/loading'
+import chatRecordModerModule from './modules/chat-record-mode'
+import scrollerModule from './modules/scroller'
+import backToTopModule from './modules/back-to-top'
+import virtualListModule from './modules/virtual-list'
+
+import Enum from './z-paging-enum'
+
+const systemInfo = u.getSystemInfoSync();
+export default {
+	name: "z-paging",
+	components: {
+		zPagingRefresh,
+		zPagingLoadMore,
+		zPagingEmptyView
+	},
+	mixins: [
+		commonLayoutModule,
+		dataHandleModule,
+		i18nModule,
+		nvueModule,
+		emptyModule,
+		refresherModule,
+		loadMoreModule,
+		loadingModule,
+		chatRecordModerModule,
+		scrollerModule,
+		backToTopModule,
+		virtualListModule
+	],
+	data() {
+		return {
+			// --------------静态资源---------------
+			base64BackToTop: zStatic.base64BackToTop,
+
+			// -------------全局数据相关--------------
+			// 当前加载类型
+			loadingType: Enum.LoadingType.Refresher,
+			requestTimeStamp: 0,
+			wxsPropType: '',
+			renderPropScrollTop: -1,
+			checkScrolledToBottomTimeOut: null,
+			cacheTopHeight: -1,
+			statusBarHeight: systemInfo.statusBarHeight,
+			scrollViewHeight: 0,
+			pagingOrgTop: -1,
+
+			// --------------状态&判断---------------
+			insideOfPaging: -1,
+			isLoadFailed: false,
+			isIos: systemInfo.platform === 'ios',
+			disabledBounce: false,
+			fromCompleteEmit: false,
+			disabledCompleteEmit: false,
+			pageLaunched: false,
+			active: false,
+			
+			// ---------------wxs相关---------------
+			wxsIsScrollTopInTopRange: true,
+			wxsScrollTop: 0,
+			wxsPageScrollTop: 0,
+			wxsOnPullingDown: false,
+		};
+	},
+	props: {
+		// 调用complete后延迟处理的时间,单位为毫秒,默认0毫秒,优先级高于minDelay
+		delay: {
+			type: [Number, String],
+			default: u.gc('delay', 0),
+		},
+		// 触发@query后最小延迟处理的时间,单位为毫秒,默认0毫秒,优先级低于delay(假设设置为300毫秒,若分页请求时间小于300毫秒,则在调用complete后延迟[300毫秒-请求时长];若请求时长大于300毫秒,则不延迟),当show-refresher-when-reload为true或reload(true)时,其最小值为400
+		minDelay: {
+			type: [Number, String],
+			default: u.gc('minDelay', 0),
+		},
+		// 设置z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
+		pagingStyle: {
+			type: Object,
+			default: u.gc('pagingStyle', {}),
+		},
+		// 设置z-paging的class,优先级低于pagingStyle和height、width、maxWidth、bgColor
+		pagingClass: {
+			type: [String, Array, Object],
+			default: u.gc('pagingClass', ''),
+		},
+		// z-paging的高度,优先级低于pagingStyle中设置的height;传字符串,如100px、100rpx、100%
+		height: {
+			type: String,
+			default: u.gc('height', '')
+		},
+		// z-paging的宽度,优先级低于pagingStyle中设置的width;传字符串,如100px、100rpx、100%
+		width: {
+			type: String,
+			default: u.gc('width', '')
+		},
+		// z-paging的最大宽度,优先级低于pagingStyle中设置的max-width;传字符串,如100px、100rpx、100%。默认为空,也就是铺满窗口宽度,若设置了特定值则会自动添加margin: 0 auto
+		maxWidth: {
+			type: String,
+			default: u.gc('maxWidth', '')
+		},
+		// z-paging的背景色,优先级低于pagingStyle中设置的background。传字符串,如"#ffffff"
+		bgColor: {
+			type: String,
+			default: u.gc('bgColor', '')
+		},
+		// 设置z-paging的容器(插槽的父view)的style
+		pagingContentStyle: {
+			type: Object,
+			default: u.gc('pagingContentStyle', {}),
+		},
+		// z-paging是否自动高度,若自动高度则会自动铺满屏幕
+		autoHeight: {
+			type: Boolean,
+			default: u.gc('autoHeight', false)
+		},
+		// z-paging是否自动高度时,附加的高度,注意添加单位px或rpx,若需要减少高度,则传负数
+		autoHeightAddition: {
+			type: [Number, String],
+			default: u.gc('autoHeightAddition', '0px')
+		},
+		// loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认black
+		defaultThemeStyle: {
+			type: String,
+			default: u.gc('defaultThemeStyle', 'black')
+		},
+		// z-paging是否使用fixed布局,若使用fixed布局,则z-paging的父view无需固定高度,z-paging高度默认为100%,默认为是(当使用内置scroll-view滚动时有效)
+		fixed: {
+			type: Boolean,
+			default: u.gc('fixed', true)
+		},
+		// 是否开启底部安全区域适配
+		safeAreaInsetBottom: {
+			type: Boolean,
+			default: u.gc('safeAreaInsetBottom', false)
+		},
+		// 开启底部安全区域适配后,是否使用placeholder形式实现,默认为否。为否时滚动区域会自动避开底部安全区域,也就是所有滚动内容都不会挡住底部安全区域,若设置为是,则滚动时滚动内容会挡住底部安全区域,但是当滚动到底部时才会避开底部安全区域
+		useSafeAreaPlaceholder: {
+			type: Boolean,
+			default: u.gc('useSafeAreaPlaceholder', false)
+		},
+		// z-paging bottom的背景色,默认透明,传字符串,如"#ffffff"
+		bottomBgColor: {
+			type: String,
+			default: u.gc('bottomBgColor', '')
+		},
+		// slot="top"的view的z-index,默认为99,仅使用页面滚动时有效
+		topZIndex: {
+			type: Number,
+			default: u.gc('topZIndex', 99)
+		},
+		// z-paging内容容器父view的z-index,默认为1
+		superContentZIndex: {
+			type: Number,
+			default: u.gc('superContentZIndex', 1)
+		},
+		// z-paging内容容器部分的z-index,默认为1
+		contentZIndex: {
+			type: Number,
+			default: u.gc('contentZIndex', 1)
+		},
+		// z-paging二楼的z-index,默认为100
+		f2ZIndex: {
+			type: Number,
+			default: u.gc('f2ZIndex', 100)
+		},
+		// 使用页面滚动时,是否在不满屏时自动填充满屏幕,默认为是
+		autoFullHeight: {
+			type: Boolean,
+			default: u.gc('autoFullHeight', true)
+		},
+		// 是否监听列表触摸方向改变,默认为否
+		watchTouchDirectionChange: {
+			type: Boolean,
+			default: u.gc('watchTouchDirectionChange', false)
+		},
+		// 是否监听列表滚动方向改变,默认为否
+		watchScrollDirectionChange: {
+			type: Boolean,
+			default: u.gc('watchScrollDirectionChange', false)
+		},
+		// 是否只使用基础布局,设置为true后将关闭mounted自动请求数据、关闭下拉刷新和滚动到底部加载更多,强制隐藏空数据图。默认为否
+		layoutOnly: {
+			type: Boolean,
+			default: u.gc('layoutOnly', false)
+		},
+		// z-paging中布局的单位,默认为rpx
+		unit: {
+			type: String,
+			default: u.gc('unit', 'rpx')
+		}
+	},
+	created() {
+		// 组件创建时,检测是否开始加载状态
+		if (this.createdReload && !this.isOnly && this.auto) {
+			this._startLoading();
+			this.$nextTick(this._preReload);
+		}
+	},
+	mounted() {
+		this.active = true;
+		this.wxsPropType = u.getTime().toString();
+		this.renderJsIgnore;
+		if (!this.createdReload && !this.isOnly && this.auto) {
+			// 开始预加载
+			u.delay(() => this.$nextTick(this._preReload), 0);
+		}
+		// 如果开启了列表缓存,在初始化的时候通过缓存数据填充列表数据
+		this.finalUseCache && this._setListByLocalCache();
+		let delay = 0;
+		// #ifdef H5 || MP
+		delay = c.delayTime;
+		// #endif
+		this.$nextTick(() => {
+			// 初始化systemInfo
+			this.systemInfo = u.getSystemInfoSync();
+			// 初始化z-paging高度
+			!this.usePageScroll && this.autoHeight  && this._setAutoHeight();
+			this.loaded = true;
+			u.delay(() => {
+				// 更新fixed模式下z-paging的布局,主要是更新windowTop、windowBottom
+				this.updateFixedLayout();
+				// 更新缓存中z-paging整个内容容器高度
+				this._updateCachedSuperContentHeight();
+				// 更新z-paging中scroll-view高度
+				this._updateScrollViewHeight();
+			});
+		})
+		// 初始化页面滚动模式下slot="top"、slot="bottom"高度
+		this.updatePageScrollTopHeight();
+		this.updatePageScrollBottomHeight();
+		// 初始化slot="left"、slot="right"宽度
+		this.updateLeftAndRightWidth();
+		if (this.finalRefresherEnabled && this.useCustomRefresher) {
+			this.$nextTick(() => {
+				this.isTouchmoving = true;
+			})
+		}
+		if (!this.layoutOnly) {
+			// 监听uni.$emit中全局emit的complete error等事件
+			this._onEmit();
+		}
+		// #ifdef APP-NVUE
+		if (!this.isIos && !this.useChatRecordMode) {
+			this.nLoadingMoreFixedHeight = true;
+		}
+		// 在nvue中更新nvue下拉刷新view容器的宽度,而不是写死默认的750rpx,需要考虑列表宽度不是铺满屏幕的情况
+		this._nUpdateRefresherWidth();
+		// #endif
+		// #ifndef APP-PLUS
+		this.$nextTick(() => {
+			// 非app平台中,在通过获取css设置的底部安全区域占位view高度设置bottom距离后,更新页面滚动底部高度
+			setTimeout(() => {
+				this._getCssSafeAreaInsetBottom(() => this.safeAreaInsetBottom && this.updatePageScrollBottomHeight());
+			}, delay)
+		})
+		// #endif
+	},
+	destroyed() {
+		this._handleUnmounted();
+	},
+	// #ifdef VUE3
+	unmounted() {
+		this._handleUnmounted();
+	},
+	// #endif
+	watch: {
+		defaultThemeStyle: {
+			handler(newVal) {
+				if (newVal.length) {
+					this.finalRefresherDefaultStyle = newVal;
+				}
+			},
+			immediate: true
+		},
+		autoHeight(newVal) {
+			this.loaded && !this.usePageScroll && this._setAutoHeight(newVal);
+		},
+		autoHeightAddition(newVal) {
+			this.loaded && !this.usePageScroll && this.autoHeight && this._setAutoHeight(newVal);
+		},
+	},
+	computed: {
+		// 当前z-paging的内置样式
+		finalPagingStyle() {
+			const pagingStyle = { ...this.pagingStyle };
+			if (!this.systemInfo) return pagingStyle;
+			const { windowTop, windowBottom } = this;
+			if (!this.usePageScroll && this.fixed) {
+				if (windowTop && !pagingStyle.top) {
+					pagingStyle.top = windowTop + 'px';
+				}
+				if (windowBottom && !pagingStyle.bottom) {
+					pagingStyle.bottom = windowBottom + 'px';
+				}
+			}
+			if (this.bgColor.length && !pagingStyle['background']) {
+				pagingStyle['background'] = this.bgColor;
+			}
+			if (this.height.length && !pagingStyle['height']) {
+				pagingStyle['height'] = this.height;
+			}
+			if (this.width.length && !pagingStyle['width']) {
+				pagingStyle['width'] = this.width;
+			}
+			if (this.maxWidth.length && !pagingStyle['max-width']) {
+				pagingStyle['max-width'] = this.maxWidth;
+				pagingStyle['margin'] = '0 auto';
+			}
+			return pagingStyle;
+		},
+		// 当前z-paging内容的样式
+		finalPagingContentStyle() {
+			if (this.contentZIndex != 1) {
+				this.pagingContentStyle['z-index'] = this.contentZIndex;
+				this.pagingContentStyle['position'] = 'relative';
+			}
+			return this.pagingContentStyle;
+		},
+		// 最终的当前开启安全区域适配后,是否使用placeholder形式实现。如果slot=bottom存在,则应当交由固定在底部的view处理,因此需排除此情况
+		finalUseSafeAreaPlaceholder() {
+			return this.useSafeAreaPlaceholder && !this.zSlots.bottom;
+		},
+		renderJsIgnore() {
+			if ((this.usePageScroll && this.useChatRecordMode) || (!this.refresherEnabled && this.scrollable) || !this.useCustomRefresher) {
+				this.$nextTick(() => {
+					this.renderPropScrollTop = 10;
+				})
+			}
+			return 0;
+		},
+		windowHeight() {
+			if (!this.systemInfo) return 0;
+			return this.systemInfo.windowHeight || 0;
+		},
+		windowBottom() {
+			if (!this.systemInfo) return 0;
+			return this.systemInfo.windowBottom || 0;
+		},
+		// 是否是ios+h5
+		isIosAndH5() {
+			// #ifndef H5
+			return false;
+			// #endif
+			return this.isIos;
+		},
+		// 是否是只使用基础布局或者只使用下拉刷新
+		isOnly() {
+			return this.layoutOnly || this.refresherOnly;
+		},
+	},
+	methods: {
+		// 当前版本号
+		getVersion() {
+			return `z-paging v${c.version}`;
+		},
+		// 设置nvue List的specialEffects
+		setSpecialEffects(args) {
+			this.setListSpecialEffects(args);
+		},
+		// 与setSpecialEffects等效,兼容旧版本
+		setListSpecialEffects(args) {
+			this.nFixFreezing = args && Object.keys(args).length;
+			if (this.isIos) {
+				this.privateRefresherEnabled = 0;
+			}
+			!this.usePageScroll && this.$refs['zp-n-list'].setSpecialEffects(args);
+		},
+		// #ifdef APP-VUE
+		// 当app长时间进入后台后进入前台,因系统内存管理导致app重新加载时,进行一些适配处理
+		_handlePageLaunch() {
+			// 首次触发不进行处理,只有进入后台后打开app重新加载时才处理
+			if (this.pageLaunched) {
+				// 解决在vue3+ios中,app ReLaunch时顶部下拉刷新展示位置向下偏移的问题
+				// #ifdef VUE3
+				this.refresherThresholdUpdateTag = 1;
+				this.$nextTick(() => {
+					this.refresherThresholdUpdateTag = 0;
+				})
+				// #endif
+				// 解决使用虚拟列表时,app ReLaunch时白屏问题
+				this._checkVirtualListScroll();
+			}
+			this.pageLaunched = true;
+		},
+		// #endif
+		// 使手机发生较短时间的振动(15ms)
+		_doVibrateShort() {
+			// #ifndef H5
+			
+			// #ifdef APP-PLUS
+			if (this.isIos) {
+				const UISelectionFeedbackGenerator = plus.ios.importClass('UISelectionFeedbackGenerator');
+				const feedbackGenerator = new UISelectionFeedbackGenerator();
+				feedbackGenerator.init();
+				setTimeout(() => {
+					feedbackGenerator.selectionChanged();
+				}, 0)
+			} else {
+				plus.device.vibrate(15);
+			}
+			// #endif
+			// #ifndef APP-PLUS
+			uni.vibrateShort();
+			// #endif
+			
+			// #endif
+		},
+		// 设置z-paging高度
+		async _setAutoHeight(shouldFullHeight = true, scrollViewNode = null) {
+			const heightKey = 'min-height';
+			try {
+				if (shouldFullHeight) {
+					// 如果需要铺满全屏,则计算当前全屏可是区域的高度
+					let finalScrollViewNode = scrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
+					let finalScrollBottomNode = await this._getNodeClientRect('.zp-page-bottom');
+					if (finalScrollViewNode) {
+						const scrollViewTop = finalScrollViewNode[0].top;
+						let scrollViewHeight = this.windowHeight - scrollViewTop;
+						scrollViewHeight -= finalScrollBottomNode ? finalScrollBottomNode[0].height : 0;
+						const additionHeight = u.convertToPx(this.autoHeightAddition);
+						// 在支付宝小程序中,添加!important会导致min-height失效,因此在支付宝小程序中需要去掉
+						let importantSuffix =  ' !important';
+						// #ifdef MP-ALIPAY
+						importantSuffix = '';
+						// #endif
+						const finalHeight = scrollViewHeight + additionHeight - (this.insideMore ? 1 : 0) + 'px' + importantSuffix;
+						this.$set(this.scrollViewStyle, heightKey, finalHeight);
+						this.$set(this.scrollViewInStyle, heightKey, finalHeight);
+					}
+				} else {
+					this.$delete(this.scrollViewStyle, heightKey);
+					this.$delete(this.scrollViewInStyle, heightKey);
+				}
+			} catch (e) {}
+		},
+		// 更新scroll-view高度
+		async _updateScrollViewHeight() {
+			const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
+			if (scrollViewNode) {
+				const scrollViewNodeHeight = scrollViewNode[0].height;
+				this.scrollViewHeight = scrollViewNodeHeight;
+				this.pagingOrgTop =  scrollViewNode[0].top;
+				// 设置scroll-view内容器的最小高度等于scroll-view的高度(为了解决在快手小程序中内容较少时scroll-view内容器高度无法铺满scroll-view的问题)
+				// #ifdef MP-KUAISHOU
+				this.$set(this.scrollViewInStyle, 'min-height', scrollViewNodeHeight + 'px');
+				// #endif
+			}
+		},
+		// 组件销毁后续处理
+		_handleUnmounted() {
+			this.active = false;
+			if (!this.layoutOnly) {
+				this._offEmit();
+			}
+			// 取消监听键盘高度变化事件(H5、百度小程序、抖音小程序、飞书小程序、QQ小程序、快手小程序不支持)
+			// #ifndef H5 || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU
+			this.useChatRecordMode && uni.offKeyboardHeightChange(this._handleKeyboardHeightChange);
+			// #endif
+		},
+		// 触发更新是否超出页面状态
+		_updateInsideOfPaging() {
+			this.insideMore && this.insideOfPaging === true && setTimeout(this.doLoadMore, 200)
+		},
+		// 清除timeout
+		_cleanTimeout(timeout) {
+			if (timeout) {
+				clearTimeout(timeout);
+				timeout = null;
+			}
+			return timeout;
+		},
+		// 添加全局emit监听
+		_onEmit() {
+			uni.$on(c.errorUpdateKey, (errorMsg) => {
+				if (this.loading) {
+					if (!!errorMsg) {
+						this.customerEmptyViewErrorText = errorMsg;
+					}
+					this.complete(false).catch(() => {});
+				}
+			})
+			uni.$on(c.completeUpdateKey, (data) => {
+				setTimeout(() => {
+					if (this.loading) {
+						if (!this.disabledCompleteEmit) {
+							const type = data.type || 'normal';
+							const list = data.list || data;
+							const rule = data.rule;
+							this.fromCompleteEmit = true;
+							switch (type){
+								case 'normal':
+									this.complete(list);
+									break;
+								case 'total':
+									this.completeByTotal(list, rule);
+									break;
+								case 'nomore':
+									this.completeByNoMore(list, rule);
+									break;
+								case 'key':
+									this.completeByKey(list, rule);
+									break;
+								default:
+									break;
+							}
+						} else {
+							this.disabledCompleteEmit = false;
+						}
+					}
+				}, 1);
+			})
+		},
+		// 销毁全局emit和listener监听
+		_offEmit(){
+			uni.$off(c.errorUpdateKey);
+			uni.$off(c.completeUpdateKey);
+		},
+	},
+};

+ 22 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-mixin.js

@@ -0,0 +1,22 @@
+// [z-paging]使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法
+
+export default {
+	onPullDownRefresh() {
+		if (this.isPagingRefNotFound()) return;
+		this.$refs.paging.reload().catch(() => {});
+	},
+	onPageScroll(e) {
+		if (this.isPagingRefNotFound()) return;
+		this.$refs.paging.updatePageScrollTop(e.scrollTop);
+		e.scrollTop < 10 && this.$refs.paging.doChatRecordLoadMore();
+	},
+	onReachBottom() {
+		if (this.isPagingRefNotFound()) return;
+		this.$refs.paging.pageReachBottom();
+	},
+	methods: {
+		isPagingRefNotFound() {
+			return !this.$refs.paging;
+		}
+	}
+}

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


+ 322 - 0
uni_modules/z-paging/components/z-paging/js/z-paging-utils.js

@@ -0,0 +1,322 @@
+// [z-paging]工具类
+
+import zLocalConfig from '../config/index'
+import c from './z-paging-constant'
+
+const storageKey = 'Z-PAGING-REFRESHER-TIME-STORAGE-KEY';
+let config = null;
+let configLoaded = false;
+let cachedSystemInfo = null;
+const timeoutMap = {};
+
+// 获取默认配置信息
+function gc(key, defaultValue) {
+	// 这里return一个函数以解决在vue3+appvue中,props默认配置读取在main.js之前执行导致uni.$zp全局配置无效的问题。相当于props的default中传入一个带有返回值的函数
+	return () => {
+		// 处理z-paging全局配置
+		_handleDefaultConfig();
+		// 如果全局配置不存在,则返回默认值
+		if (!config) return defaultValue;
+		const value = config[key];
+		// 如果全局配置存在但对应的配置项不存在,则返回默认值;反之返回配置项
+		return value === undefined ? defaultValue : value;
+	};
+}
+
+// 获取最终的touch位置
+function getTouch(e) {
+	let touch = null;
+	if (e.touches && e.touches.length) {
+		touch = e.touches[0];
+	} else if (e.changedTouches && e.changedTouches.length) {
+		touch = e.changedTouches[0];
+	} else if (e.datail && e.datail != {}) {
+		touch = e.datail;
+	} else {
+		return { touchX: 0, touchY: 0 }
+	}
+	return {
+		touchX: touch.clientX,
+		touchY: touch.clientY
+	};
+}
+
+// 判断当前手势是否在z-paging内触发
+function getTouchFromZPaging(target) {
+	if (target && target.tagName && target.tagName !== 'BODY' && target.tagName !== 'UNI-PAGE-BODY') {
+		const classList = target.classList;
+		if (classList && classList.contains('z-paging-content')) {
+			// 此处额外记录当前z-paging是否是页面滚动、是否滚动到了顶部、是否是聊天记录模式以传给renderjs。避免不同z-paging组件renderjs内部判断数据互相影响导致的各种问题
+			return {
+				isFromZp: true,
+				isPageScroll: classList.contains('z-paging-content-page'),
+				isReachedTop: classList.contains('z-paging-reached-top'),
+				isUseChatRecordMode: classList.contains('z-paging-use-chat-record-mode')
+			};
+		} else {
+			return getTouchFromZPaging(target.parentNode);
+		}
+	} else {
+		return { isFromZp: false };
+	}
+}
+
+// 递归获取z-paging所在的parent,如果查找不到则返回null
+function getParent(parent) {
+	if (!parent) return null;
+	if (parent.$refs.paging) return parent;
+	return getParent(parent.$parent);
+}
+
+// 打印错误信息
+function consoleErr(err) {
+	console.error(`[z-paging]${err}`);
+}
+
+// 延时操作,如果key存在,调用时清除对应key之前的延时操作
+function delay(callback, ms = c.delayTime, key) {
+	const timeout = setTimeout(callback, ms);;
+	if (!!key) {
+		timeoutMap[key] && clearTimeout(timeoutMap[key]);
+		timeoutMap[key] = timeout;
+	}
+	return timeout;
+}
+
+// 设置下拉刷新时间
+function setRefesrherTime(time, key) {
+	const datas = getRefesrherTime() || {};
+	datas[key] = time;
+	uni.setStorageSync(storageKey, datas);
+}
+
+// 获取下拉刷新时间
+function getRefesrherTime() {
+	return uni.getStorageSync(storageKey);
+}
+
+// 通过下拉刷新标识key获取下拉刷新时间
+function getRefesrherTimeByKey(key) {
+	const datas = getRefesrherTime();
+	return datas && datas[key] ? datas[key] : null;
+}
+
+// 通过下拉刷新标识key获取下拉刷新时间(格式化之后)
+function getRefesrherFormatTimeByKey(key, textMap) {
+	const time = getRefesrherTimeByKey(key);
+	const timeText = time ? _timeFormat(time, textMap) : textMap.none;
+	return `${textMap.title}${timeText}`;
+}
+
+// 将文本的px或者rpx转为px的值
+function convertToPx(text) {
+	const dataType = Object.prototype.toString.call(text);
+	if (dataType === '[object Number]') return text;
+	let isRpx = false;
+	if (text.indexOf('rpx') !== -1 || text.indexOf('upx') !== -1) {
+		text = text.replace('rpx', '').replace('upx', '');
+		isRpx = true;
+	} else if (text.indexOf('px') !== -1) {
+		text = text.replace('px', '');
+	}
+	if (!isNaN(text)) {
+		if (isRpx) return Number(rpx2px(text));
+		return Number(text);
+	}
+	return 0;
+}
+
+// rpx => px,预留的兼容处理
+function rpx2px(rpx) {
+	return uni.upx2px(rpx);
+}
+
+// 同步获取系统信息,兼容不同平台
+function getSystemInfoSync(useCache = false) {
+	if (useCache && cachedSystemInfo) {
+		return cachedSystemInfo;
+	}
+	// 目前只用到了deviceInfo、appBaseInfo和windowInfo中的信息,因此仅整合这两个信息数据
+	const infoTypes = ['DeviceInfo', 'AppBaseInfo', 'WindowInfo'];
+	const { deviceInfo, appBaseInfo, windowInfo } = infoTypes.reduce((acc, key) => {
+		const method = `get${key}`;
+		if (uni[method] && uni.canIUse(method)) {
+			acc[key.charAt(0).toLowerCase() + key.slice(1)] = uni[method]();
+		}
+		return acc;
+	}, {});
+	// 如果deviceInfo、appBaseInfo和windowInfo都可以从各自专属的api中获取,则整合它们的数据
+	if (deviceInfo && appBaseInfo && windowInfo) {
+		cachedSystemInfo = { ...deviceInfo, ...appBaseInfo, ...windowInfo };
+	} else {
+		// 使用uni.getSystemInfoSync兜底,确保能获取到最终的系统信息
+		cachedSystemInfo = uni.getSystemInfoSync();
+	}
+	return cachedSystemInfo;
+}
+
+// 获取当前时间
+function getTime() {
+	return (new Date()).getTime();
+}
+
+// 获取z-paging实例id,随机生成10位数字+字母
+function getInstanceId() {
+	const s = [];
+	const hexDigits = "0123456789abcdef";
+	for (let i = 0; i < 10; i++) {
+		s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
+	}
+	return s.join('') + getTime();
+}
+
+// 等待一段时间
+function wait(ms) {
+	return new Promise(resolve => {
+		setTimeout(resolve, ms);
+	});
+}
+
+// 是否是promise
+function isPromise(func) {
+	return Object.prototype.toString.call(func) === '[object Promise]';
+}
+
+// 添加单位
+function addUnit(value, unit) {
+	if (Object.prototype.toString.call(value) === '[object String]') {
+		let tempValue = value;
+		tempValue = tempValue.replace('rpx', '').replace('upx', '').replace('px', '');
+		if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1 && value.indexOf('px') !== -1) {
+			tempValue = parseFloat(tempValue) * 2;
+		}
+		value = tempValue;
+	}
+	return unit === 'rpx' ? value + 'rpx' : (value / 2) + 'px';
+}
+
+// 深拷贝
+function deepCopy(obj) {
+	if (typeof obj !== 'object' || obj === null) return obj;
+	let newObj = Array.isArray(obj) ? [] : {};
+	for (let key in obj) {
+		if (obj.hasOwnProperty(key)) {
+			newObj[key] = deepCopy(obj[key]);
+		}
+	}
+	return newObj;
+}
+
+// 对短时间内重复插入的数据进行整合,并一次性插入
+function useBufferedInsert(fn, delay = 50) {
+	let buffer = [];
+	let timer = null;
+	let latestArgs = [];
+	return function insertBuffered(data, ...args) {
+		const newData = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : data;
+		buffer.push(...newData);
+		latestArgs = args;
+		if (!timer) {
+			timer = setTimeout(() => {
+				fn(buffer.length === 1 ? buffer[0] : buffer, ...latestArgs);
+				buffer = [];
+				timer = null;
+			}, buffer.length === 1 ? 10 : delay);
+		}
+	};
+}
+
+// ------------------ 私有方法 ------------------------
+// 处理全局配置
+function _handleDefaultConfig() {
+	// 确保只加载一次全局配置
+	if (configLoaded) return;
+	// 优先从config.js中读取
+	if (zLocalConfig && Object.keys(zLocalConfig).length) {
+		config = zLocalConfig;
+	}
+	// 如果在config.js中读取不到,则尝试到uni.$zp读取
+	if (!config && uni.$zp) {
+		config = uni.$zp.config;
+	}
+	// 将config中的短横线写法全部转为驼峰写法,使得读取配置时可以直接通过key去匹配,而非读取每个配置时候再去转,减少不必要的性能开支
+	config = config ? Object.keys(config).reduce((result, key) => {
+		result[_toCamelCase(key)] = config[key];
+		return result;
+	}, {}) : null;
+	configLoaded = true;
+}
+
+// 时间格式化
+function _timeFormat(time, textMap) {
+	const date = new Date(time);
+	const currentDate = new Date();
+	// 设置time对应的天,去除时分秒,使得可以直接比较日期
+	const dateDay = new Date(time).setHours(0, 0, 0, 0);
+	// 设置当前的天,去除时分秒,使得可以直接比较日期
+	const currentDateDay = new Date().setHours(0, 0, 0, 0);
+	const disTime = dateDay - currentDateDay;
+	let dayStr = '';
+	const timeStr = _dateTimeFormat(date);
+	if (disTime === 0) {
+		dayStr = textMap.today;
+	} else if (disTime === -86400000) {
+		dayStr = textMap.yesterday;
+	} else {
+		dayStr = _dateDayFormat(date, date.getFullYear() !== currentDate.getFullYear());
+	}
+	return `${dayStr} ${timeStr}`;
+}
+
+// date格式化为年月日
+function _dateDayFormat(date, showYear = true) {
+	const year = date.getFullYear();
+	const month = date.getMonth() + 1;
+	const day = date.getDate();
+	return showYear ? `${year}-${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}` : `${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}`;
+}
+
+// data格式化为时分
+function _dateTimeFormat(date) {
+	const hour = date.getHours();
+	const minute = date.getMinutes();
+	return `${_fullZeroToTwo(hour)}:${_fullZeroToTwo(minute)}`;
+}
+
+// 不满2位在前面填充0
+function _fullZeroToTwo(str) {
+	str = str.toString();
+	return str.length === 1 ? '0' + str : str;
+}
+
+// 驼峰转短横线
+function _toKebab(value) {
+	return value.replace(/([A-Z])/g, "-$1").toLowerCase();
+}
+
+// 短横线转驼峰
+function _toCamelCase(value) {
+	return value.replace(/-([a-z])/g, (_, group1) => group1.toUpperCase());
+}
+
+
+export default {
+	gc,
+	setRefesrherTime,
+	getRefesrherFormatTimeByKey,
+	getTouch,
+	getTouchFromZPaging,
+	getParent,
+	convertToPx,
+	getTime,
+	getInstanceId,
+	consoleErr,
+	delay,
+	wait,
+	isPromise,
+	addUnit,
+	deepCopy,
+	rpx2px,
+	getSystemInfoSync,
+	useBufferedInsert
+};

+ 67 - 0
uni_modules/z-paging/components/z-paging/wxs/z-paging-renderjs.js

@@ -0,0 +1,67 @@
+// [z-paging]使用renderjs在app-vue和h5中对touchmove事件冒泡进行处理
+
+import u from '../js/z-paging-utils'
+const data = {
+	startY: 0,
+	isTouchFromZPaging: false,
+	isUsePageScroll: false,
+	isReachedTop: true,
+	isIosAndH5: false,
+	useChatRecordMode: false,
+	appLaunched: false
+}
+
+export default {
+	mounted() {
+		if (window) {
+			this._handleTouch();
+			// #ifdef APP-VUE
+			this.$ownerInstance.callMethod('_handlePageLaunch');
+			// #endif
+		}
+	},
+	methods: {
+		// 接收逻辑层发送的数据(是否是ios+h5)
+		renderPropIsIosAndH5Change(newVal) {
+			if (newVal === -1) return;
+			data.isIosAndH5 = newVal;
+		},
+
+		// 拦截处理touch事件
+		_handleTouch() {
+			if (!window.$zPagingRenderJsInited) {
+				window.$zPagingRenderJsInited = true;
+				window.addEventListener('touchstart', this._handleTouchstart, { passive: true })
+				window.addEventListener('touchmove', this._handleTouchmove, { passive: false })
+			}
+		},
+		// 处理touch开始
+		_handleTouchstart(e) {
+			const touch = u.getTouch(e);
+			data.startY = touch.touchY;
+			const touchResult = u.getTouchFromZPaging(e.target);
+			data.isTouchFromZPaging = touchResult.isFromZp;
+			data.isUsePageScroll = touchResult.isPageScroll;
+			data.isReachedTop = touchResult.isReachedTop;
+			data.useChatRecordMode = touchResult.isUseChatRecordMode;
+		},
+		// 处理touch中
+		_handleTouchmove(e) {
+			const touch = u.getTouch(e);
+			const moveY = touch.touchY - data.startY;
+			// 如果是在z-paging内触摸并且(是在顶部位置且是下拉的情况下(或不是聊天记录滚动模式并且在iOS+h5+scroll-view并且是往上拉的情况:避免在此平台中滚动到底部后上拉有个系统灰色遮罩导致列表被短暂锁定的问题))
+			// (data.useChatRecordMode ? moveY < 0 : moveY > 0)是为了判断是否是上拉的情况,聊天记录模式列表倒置,因此moveY < 0为上拉
+			if (data.isTouchFromZPaging && ((data.isReachedTop && (data.useChatRecordMode ? moveY < 0 : moveY > 0)) || (!data.useChatRecordMode && data.isIosAndH5 && !data.isUsePageScroll && moveY < 0))) {
+				if (e.cancelable && !e.defaultPrevented) {
+					// 阻止事件冒泡,以避免在一些平台中下拉刷新时整个page跟着一起下拉&在iOS+h5+scroll-view中在底部上拉有个系统灰色遮罩导致列表被短暂锁定的问题
+					e.preventDefault();
+				}
+			}
+		},
+		// 移除touch相关事件监听
+		_removeAllEventListener(){
+			window.removeEventListener('touchstart');
+			window.removeEventListener('touchmove');
+		}
+	}
+};

+ 382 - 0
uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs

@@ -0,0 +1,382 @@
+// [z-paging]微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
+
+var currentDis = 0;
+var isPCFlag = -1;
+var startY = -1;
+
+// 监听js层传过来的数据
+function propObserver(newVal, oldVal, ownerIns, ins) {
+	var state = ownerIns.getState() || {};
+	state.currentIns = ins;
+	var dataset = ins.getDataset();
+	var loading = dataset.loading == true;
+	// 如果是下拉刷新结束,更新transform
+	if (newVal && newVal.indexOf('end') != -1) {
+		var transition = newVal.split('end')[0];
+		_setTransform('translateY(0px)', ins, false, transition);
+		state.moveDis = 0;
+		state.oldMoveDis = 0;
+		currentDis = 0;
+	} else if (newVal && newVal.indexOf('begin') != -1) {
+		// 如果是下拉刷新开始,更新transform
+		var refresherThreshold = ins.getDataset().refresherthreshold;
+		_setTransformValue(refresherThreshold, ins, state, false);
+	}
+}
+
+// touch开始
+function touchstart(e, ownerIns) {
+	var ins = _getIns(ownerIns);
+	var state = {};
+	var dataset = {};
+	ownerIns.callMethod('_handleListTouchstart');
+	if (ins) {
+		state = ins.getState();
+		dataset = ins.getDataset();
+		if (_touchDisabled(e, ins, 0)) return;
+	}
+	var isTouchEnded = state.isTouchEnded;
+	state.oldMoveDis = 0;
+	var touch = _getTouch(e);
+	var loading = _isTrue(dataset.loading);
+	state.startY = touch.touchY;
+	startY = state.startY;
+	state.lastTouch = touch;
+	if (!loading && isTouchEnded) {
+		state.isTouchmoving = false;
+	}
+	state.isTouchEnded = false;
+	// 通知js层touch开始
+	ownerIns.callMethod('_handleRefresherTouchstart', touch);
+}
+
+// touch中
+function touchmove(e, ownerIns) {
+	var touch = _getTouch(e);
+	var ins = _getIns(ownerIns);
+	var dataset = ins.getDataset();
+	var refresherThreshold = dataset.refresherthreshold;
+	var refresherF2Threshold = dataset.refresherf2threshold;
+	var refresherF2Enabled = _isTrue(dataset.refresherf2enabled);
+	var isIos = _isTrue(dataset.isios);
+	var state = ins.getState();
+	var watchTouchDirectionChange = _isTrue(dataset.watchtouchdirectionchange);
+	var moveDisObj = {};
+	var moveDis = 0;
+	var prevent = false;
+	// 如果需要监听touch方向的改变
+	if (watchTouchDirectionChange) {
+		moveDisObj = _getMoveDis(e, ins);
+		moveDis = moveDisObj.currentDis;
+		prevent = moveDisObj.isDown;
+		var direction = prevent ? 'top' : 'bottom';
+		// 确保只在touch方向改变时通知一次js层,而不是touchmove中持续通知
+		if (prevent == state.oldTouchDirection && prevent != state.oldEmitedTouchDirection) {
+			ownerIns.callMethod('_handleTouchDirectionChange', { direction: direction }); 
+			state.oldEmitedTouchDirection = prevent;
+		}
+		state.oldTouchDirection = prevent;
+	}
+	// 判断是否允许下拉刷新
+	if (_touchDisabled(e, ins, 1)) {
+		_handlePullingDown(state, ownerIns, false);
+		return true;
+	}
+	// 判断下拉刷新的角度是否在要求范围内
+	if (!_getAngleIsInRange(e, touch, state, dataset)) {
+		_handlePullingDown(state, ownerIns, false);
+		return true;
+	}
+	moveDisObj = _getMoveDis(e, ins);
+	moveDis = moveDisObj.currentDis;
+	prevent = moveDisObj.isDown;
+	if (moveDis < 0) {
+		// moveDis小于0,将transform重置为0
+		_setTransformValue(0, ins, state, false);
+		_handlePullingDown(state, ownerIns, false);
+		return true;
+	}
+	if (prevent && !state.disabledBounce) {
+		// 如果是用户下拉并且需要触发下拉刷新,需要通知js层将列表禁止滚动,防止在下拉刷新过程中列表也可以滚动导致的下拉刷新偏移过大的问题(在下拉刷新过程中仅通知一次)
+		ownerIns.callMethod('_handleScrollViewBounce', { bounce: false });
+		state.disabledBounce = true;
+		_handlePullingDown(state, ownerIns, prevent);
+		return !prevent;
+	}
+	// 更新transform
+	_setTransformValue(moveDis, ins, state, false);
+	var oldRefresherStatus = state.refresherStatus;
+	var oldIsTouchmoving = _isTrue(dataset.oldistouchmoving);
+	var hasTouchmove = _isTrue(dataset.hastouchmove);
+	var isTouchmoving = state.isTouchmoving;
+	state.refresherStatus = moveDis >= refresherThreshold ? (refresherF2Enabled && moveDis > refresherF2Threshold ? 'goF2' : 'releaseToRefresh') : 'default';
+	if (!isTouchmoving) {
+		state.isTouchmoving = true;
+		isTouchmoving = true;
+	}
+	if (state.isTouchEnded) {
+		state.isTouchEnded = false;
+	}
+	// 如果需要实时监听下拉位置偏移,则需要实时通知js层,此操作会使wxs层与js层频繁通信从而导致在一些性能较差设备中下拉刷新卡顿
+	if (hasTouchmove) {
+		ownerIns.callMethod('_handleWxsPullingDown', { moveDis: moveDis, diffDis: moveDisObj.diffDis });
+	}
+	// 在下拉刷新状态改变时通知js层
+	if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving != isTouchmoving) {
+		ownerIns.callMethod('_handleRefresherTouchmove', moveDis, touch);
+	}
+	_handlePullingDown(state, ownerIns, prevent);
+	return !prevent;
+}
+
+// touch结束
+function touchend(e, ownerIns) {
+	var touch = _getTouch(e);
+	var ins = _getIns(ownerIns);
+	var dataset = ins.getDataset();
+	var state = ins.getState();
+	if (state.disabledBounce) {
+		// 通知js允许列表滚动
+		ownerIns.callMethod('_handleScrollViewBounce', { bounce: true });
+		state.disabledBounce = false;
+	}
+	if (_touchDisabled(e, ins, 2)) return;
+	state.reachMaxAngle = true;
+	state.hitReachMaxAngleCount = 0;
+	state.fixedIsTopHitCount = 0;
+	if (!state.isTouchmoving) return;
+	var oldRefresherStatus = state.refresherStatus;
+	var oldMoveDis = state.moveDis;
+	var refresherThreshold = ins.getDataset().refresherthreshold;
+	var moveDis = _getMoveDis(e, ins).currentDis;
+	if (!(moveDis >= refresherThreshold && oldRefresherStatus === 'releaseToRefresh')) {
+		state.isTouchmoving = false;
+	}
+	// 通知js层touch结束
+	ownerIns.callMethod('_handleRefresherTouchend', moveDis);
+	state.isTouchEnded = true;
+	if (oldMoveDis < refresherThreshold) return;
+	var animate = false;
+	if (moveDis >= refresherThreshold) {
+		moveDis = refresherThreshold;
+		animate = true;
+	}
+	_setTransformValue(moveDis, ins, state, animate);
+}
+
+// #ifdef H5
+// 判断是否是pc平台
+function isPC() {
+	if (!navigator) return false;
+	if (isPCFlag != -1) return isPCFlag;
+	var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
+	isPCFlag = agents.every(function(item) { return navigator.userAgent.indexOf(item) < 0 });
+	return isPCFlag;
+}
+
+var movable = false;
+
+// 在pc平台监听mousedown、mousemove、mouseup等相关事件并转为对应touch事件处理,使得在pc平台也支持通过鼠标进行下拉刷新
+
+function mousedown(e, ins) {
+	if (!isPC()) return;
+	touchstart(e, ins);
+	movable = true;
+}
+
+function mousemove(e, ins) {
+	if (!isPC() || !movable) return;
+	touchmove(e, ins);
+}
+
+function mouseup(e, ins) {
+	if (!isPC()) return;
+	touchend(e, ins);
+	movable = false;
+}
+
+function mouseleave(e, ins) {
+	if (!isPC()) return;
+	movable = false;
+}
+// #endif
+
+
+// 修改视图层transform
+function _setTransformValue(value, ins, state, animate) {
+	value = value || 0;
+	if (state.moveDis == value) return;
+	state.moveDis = value;
+	_setTransform('translateY(' + value + 'px)', ins, animate, '');
+}
+
+// 设置视图层transform,直接在视图层操作下拉刷新,使得js层不需要频繁和视图层通信,从而大大提升下拉刷新性能
+function _setTransform(transform, ins, animate, transition) {
+	var dataset = ins.getDataset();
+	if (_isTrue(dataset.refreshernotransform)) return;
+	transform = transform == 'translateY(0px)' ? 'none' : transform;
+	ins.requestAnimationFrame(function() {
+		var stl = { 'transform': transform };
+		if (animate) {
+			stl['transition'] = 'transform .1s linear';
+		}
+		if (transition.length) {
+			stl['transition'] = transition;
+		}
+		ins.setStyle(stl);
+	})
+}
+
+// 进一步处理下拉刷新的偏移数据
+function _getMoveDis(e, ins) {
+	var state = ins.getState();
+	var refresherThreshold = parseFloat(ins.getDataset().refresherthreshold);
+	var refresherOutRate = parseFloat(ins.getDataset().refresheroutrate);
+	var refresherPullRate = parseFloat(ins.getDataset().refresherpullrate);
+	var touch = _getTouch(e);
+	var currentStartY = !state.startY || state.startY == 'NaN' ? startY : state.startY;
+	var moveDis = touch.touchY - currentStartY;
+	var oldMoveDis = state.oldMoveDis || 0;
+	state.oldMoveDis = moveDis;
+	// 获取当前下拉刷新位置与上次的偏移量
+	var diffDis = moveDis - oldMoveDis;
+	if (diffDis > 0) {
+		// 对偏移量进行进一步处理,通过refresherPullRate等配置进行约束
+		diffDis = diffDis * refresherPullRate;
+		if (currentDis > refresherThreshold) {
+			diffDis = diffDis * (1 - refresherOutRate);
+		}
+	}
+	// 控制diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
+	diffDis = diffDis > 100 ? diffDis / 100 : (diffDis > 20 ? diffDis / 2.2 : diffDis);
+	currentDis += diffDis;
+	currentDis = Math.max(0, currentDis);
+	return {
+		currentDis: currentDis,
+		diffDis: diffDis,
+		isDown: diffDis > 0
+	};
+}
+
+// 获取经过统一格式包装的当前touch对象
+function _getTouch(e) {
+	var touch = e;
+	if (e.touches && e.touches.length) {
+		touch = e.touches[0];
+	} else if (e.changedTouches && e.changedTouches.length) {
+		touch = e.changedTouches[0];
+	} else if (e.datail && e.datail != {}) {
+		touch = e.datail;
+	}
+	return {
+		touchX: touch.clientX,
+		touchY: touch.clientY
+	};
+}
+
+// 获取当前currentIns
+function _getIns(ownerIns) {
+	var ins = ownerIns.getState().currentIns;
+	if (!ins) {
+		ownerIns.callMethod('_handlePropUpdate');
+	}
+	return ins;
+}
+
+// 判断当前状态是否允许下拉刷新
+function _touchDisabled(e, ins, processTag) {
+	var dataset = ins.getDataset();
+	var state = ins.getState();
+	var loading = _isTrue(dataset.loading);
+	var useChatRecordMode = _isTrue(dataset.usechatrecordmode);
+	var refresherEnabled = _isTrue(dataset.refresherenabled);
+	var useCustomRefresher = _isTrue(dataset.usecustomrefresher);
+	var usePageScroll = _isTrue(dataset.usepagescroll);
+	var pageScrollTop = parseFloat(dataset.pagescrolltop);
+	var scrollTop = parseFloat(dataset.scrolltop);
+	var finalScrollTop = usePageScroll ? pageScrollTop : scrollTop;
+	var fixedIsTop = false;
+	// 是否要处理滚动到顶部scrollTop不为0时候的容错,为解决在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
+	var handleFaultTolerantMove = false;
+	if (handleFaultTolerantMove && finalScrollTop == (state.startScrollTop || 0) && finalScrollTop <= 105) {
+		fixedIsTop = true;
+	}
+	var fixedIsTopHitCount = state.fixedIsTopHitCount || 0;
+	if (fixedIsTop) {
+		fixedIsTopHitCount ++;
+		if (fixedIsTopHitCount <= 2) {
+			fixedIsTop = false;
+		}
+		state.fixedIsTopHitCount = fixedIsTopHitCount;
+	} else {
+		state.fixedIsTopHitCount = 0;
+	}
+	if (handleFaultTolerantMove && processTag === 0) {
+		state.startScrollTop = finalScrollTop || 0;
+	}
+	if (handleFaultTolerantMove && processTag === 2) {
+		fixedIsTop = true;
+	}
+	return loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher || 
+	((usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) || 
+	((!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
+}
+
+// 判断下拉刷新的角度是否在要求范围内
+function _getAngleIsInRange(e, touch, state, dataset) {
+	var maxAngle = dataset.refreshermaxangle;
+	var refresherAecc = _isTrue(dataset.refresheraecc);
+	var lastTouch = state.lastTouch;
+	var reachMaxAngle = state.reachMaxAngle;
+	var moveDis = state.oldMoveDis;
+	if (!lastTouch) return true;
+	if (maxAngle >= 0 && maxAngle <= 90 && lastTouch) {
+		// 考虑下拉刷新手势由水平移动转为垂直方向移动的情况,此时不应当只判断垂直方向角度是否符合要求,应当直接禁止以避免在swiper中使用下拉刷新时,横向切换swiper途中手未离开屏幕还可以下拉刷新的问题
+		if ((!moveDis || moveDis < 1) && !refresherAecc && reachMaxAngle != null && !reachMaxAngle) return false;
+		var x = Math.abs(touch.touchX - lastTouch.touchX);
+		var y = Math.abs(touch.touchY - lastTouch.touchY);
+		var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+		if ((x || y) && x > 1) {
+			// 获取下拉刷新前后两次位移的角度
+			var angle = Math.asin(y / z) / Math.PI * 180;
+			if (angle < maxAngle) {
+				// 如果角度小于配置要求,则return,同时通过hitReachMaxAngleCount控制角度判断的灵敏程度以最大程度兼容各种使用场景
+				var hitReachMaxAngleCount = state.hitReachMaxAngleCount || 0;
+				state.hitReachMaxAngleCount = ++hitReachMaxAngleCount;
+				if (state.hitReachMaxAngleCount > 2) {
+					state.lastTouch = touch;
+					state.reachMaxAngle = false;
+				}
+				return false;
+			}
+		}
+	}
+	state.lastTouch = touch;
+	return true;
+}
+
+// 进一步处理是否在下拉刷新并通知js层
+function _handlePullingDown(state, ins, onPullingDown) {
+	var oldOnPullingDown = state.onPullingDown || false;
+	if (oldOnPullingDown != onPullingDown) {
+		ins.callMethod('_handleWxsPullingDownStatusChange', onPullingDown);
+	}
+	state.onPullingDown = onPullingDown;
+}
+
+// 判断js层传过来的值是否为true
+function _isTrue(value) {
+	value = (typeof(value) === 'string' ? JSON.parse(value) : value) || false;
+	return value == true || value == 'true';
+}
+
+module.exports = {
+	touchstart: touchstart,
+	touchmove: touchmove,
+	touchend: touchend,
+	mousedown: mousedown,
+	mousemove: mousemove,
+	mouseup: mouseup,
+	mouseleave: mouseleave,
+	propObserver: propObserver
+}

+ 656 - 0
uni_modules/z-paging/components/z-paging/z-paging.vue

@@ -0,0 +1,656 @@
+ <!--                        _             
+  ____     _ __   __ _  __ _(_)_ __   __ _ 
+ |_  /____| '_ \ / _` |/ _` | | '_ \ / _` |
+  / /_____| |_) | (_| | (_| | | | | | (_| |
+ /___|    | .__/ \__,_|\__, |_|_| |_|\__, |
+          |_|          |___/         |___/ 
+v2.8.7 (2025-05-30)
+@author ZXLee <admin@zxlee.cn>
+-->
+<!-- 文档地址:https://z-paging.zxlee.cn -->
+<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
+<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
+<!-- 反馈QQ群:343409055 -->
+
+<template name="z-paging">
+	<!-- #ifndef APP-NVUE -->
+	<view :class="[{'z-paging-content':true,'z-paging-content-full':!usePageScroll,'z-paging-content-fixed':!usePageScroll&&fixed,'z-paging-content-page':usePageScroll,'z-paging-reached-top':renderPropScrollTop<1,'z-paging-use-chat-record-mode':useChatRecordMode}, pagingClass]" :style="[finalPagingStyle]">
+		<!-- #ifndef APP-PLUS -->
+		<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
+		<!-- #endif -->
+		<!-- 二楼view -->
+		<view v-if="showF2 && showRefresherF2" @touchmove.stop.prevent class="zp-f2-content" :style="[{'transform': f2Transform, 'transition': `transform .2s linear`, 'height': superContentHeight + 'px', 'z-index': f2ZIndex}]">
+			<slot name="f2"/>
+		</view>
+		<!-- 顶部固定的slot -->
+		<template v-if="zSlots.top">
+			<slot v-if="!usePageScroll" name="top" />
+			<view v-else class="zp-page-top" @touchmove.stop.prevent :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
+				<slot name="top" />
+			</view>
+		</template>
+		<view :class="{'zp-view-super':true,'zp-scroll-view-super':!usePageScroll}" :style="[finalScrollViewStyle]">
+			<view v-if="zSlots.left" :class="{'zp-page-left':true,'zp-absoulte':finalIsOldWebView}">
+				<slot name="left" />
+			</view>
+			<view :class="{'zp-scroll-view-container':true,'zp-absoulte':finalIsOldWebView}" :style="[scrollViewContainerStyle]">
+				<scroll-view
+					ref="zp-scroll-view" :class="{'zp-scroll-view':true,'zp-scroll-view-absolute':!usePageScroll,'zp-scroll-view-hide-scrollbar':!showScrollbar}" :style="[chatRecordRotateStyle]"
+					:scroll-top="scrollTop" :scroll-left="scrollLeft" :scroll-x="scrollX"
+					:scroll-y="finalScrollable" :enable-back-to-top="finalEnableBackToTop"
+					:show-scrollbar="showScrollbar" :scroll-with-animation="finalScrollWithAnimation"
+					:scroll-into-view="scrollIntoView" :lower-threshold="finalLowerThreshold" :upper-threshold="5"
+					:refresher-enabled="finalRefresherEnabled&&!useCustomRefresher" :refresher-threshold="finalRefresherThreshold"
+					:refresher-default-style="finalRefresherDefaultStyle" :refresher-background="refresherBackground"
+					:refresher-triggered="finalRefresherTriggered" @scroll="_scroll" @scrolltolower="_onScrollToLower"
+					@scrolltoupper="_onScrollToUpper" @refresherrestore="_onRestore" @refresherrefresh="_onRefresh(true)"
+					>	
+					<view class="zp-paging-touch-view"
+					<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ  || H5 -->
+					@touchstart="_refresherTouchstart" @touchmove="_refresherTouchmove" @touchend="_refresherTouchend" @touchcancel="_refresherTouchend"
+					<!-- #endif -->
+					<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
+					@touchstart="pagingWxs.touchstart" @touchmove="pagingWxs.touchmove" @touchend="pagingWxs.touchend" @touchcancel="pagingWxs.touchend"
+					@mousedown="pagingWxs.mousedown" @mousemove="pagingWxs.mousemove" @mouseup="pagingWxs.mouseup" @mouseleave="pagingWxs.mouseleave"
+					<!-- #endif -->
+					>	
+						<view v-if="finalRefresherFixedBacHeight>0" class="zp-fixed-bac-view" :style="[{'background': refresherFixedBackground,'height': `${finalRefresherFixedBacHeight}px`}]"></view>
+						<view class="zp-paging-main" :style="[scrollViewInStyle,{'transform': finalRefresherTransform,'transition': refresherTransition}]"
+						<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
+						:change:prop="pagingWxs.propObserver" :prop="wxsPropType"
+						:data-refresherThreshold="finalRefresherThreshold" :data-refresherF2Enabled="refresherF2Enabled" :data-refresherF2Threshold="finalRefresherF2Threshold" :data-isIos="isIos"
+						:data-loading="loading||isRefresherInComplete" :data-useChatRecordMode="useChatRecordMode" 
+						:data-refresherEnabled="finalRefresherEnabled" :data-useCustomRefresher="useCustomRefresher" :data-pageScrollTop="wxsPageScrollTop"
+						:data-scrollTop="wxsScrollTop" :data-refresherMaxAngle="refresherMaxAngle" :data-refresherNoTransform="refresherNoTransform"
+						:data-refresherAecc="refresherAngleEnableChangeContinued" :data-usePageScroll="usePageScroll" :data-watchTouchDirectionChange="watchTouchDirectionChange"
+						:data-oldIsTouchmoving="isTouchmoving" :data-refresherOutRate="finalRefresherOutRate" :data-refresherPullRate="finalRefresherPullRate" :data-hasTouchmove="hasTouchmove"
+						<!-- #endif -->
+						<!-- #ifdef APP-VUE || H5 -->
+						:change:renderPropIsIosAndH5="pagingRenderjs.renderPropIsIosAndH5Change" :renderPropIsIosAndH5="isIosAndH5"
+						<!-- #endif -->
+						>	
+							<view v-if="showRefresher" class="zp-custom-refresher-view" :style="[{'margin-top': `-${finalRefresherThreshold+refresherThresholdUpdateTag}px`,'background': refresherBackground,'opacity': isTouchmoving ? 1 : 0}]">
+								<view class="zp-custom-refresher-container" :style="[{'height': `${finalRefresherThreshold}px`,'background': refresherBackground}]">
+									<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
+									<!-- 下拉刷新view -->
+									<view class="zp-custom-refresher-slot-view">
+										<slot v-if="!(zSlots.refresherComplete&&refresherStatus===R.Complete)&&!(zSlots.refresherF2&&refresherStatus===R.GoF2)" :refresherStatus="refresherStatus" name="refresher" />
+									</view>
+									<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+									<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
+									<z-paging-refresh ref="refresh" v-else-if="!showCustomRefresher" class="zp-custom-refresher-refresh" :style="[{'height': `${finalRefresherThreshold - finalRefresherThresholdPlaceholder}px`}]" :status="refresherStatus"
+										:defaultThemeStyle="finalRefresherThemeStyle" :defaultText="finalRefresherDefaultText" :isIos="isIos"
+										:pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
+										:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
+										:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+										:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
+								</view>
+							</view>
+							<view class="zp-paging-container" :style="[{justifyContent:useChatRecordMode?'flex-end':'flex-start'}]">
+								<!-- 全屏Loading -->
+								<slot v-if="showLoading&&zSlots.loading&&!loadingFullFixed" name="loading" />
+								<!-- 主体内容 -->
+								<view class="zp-paging-container-content" :style="[finalPlaceholderTopHeightStyle,finalPagingContentStyle]">
+									<!-- #ifdef VUE3 -->
+									<!-- 虚拟列表顶部占位view -->
+									<view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderTopHeight+'px'}]"/>
+									<!-- #endif -->
+									<slot />
+									<!-- 内置列表&虚拟列表 -->
+									<template v-if="finalUseInnerList">
+										<slot name="header"/>
+										<view class="zp-list-container" :style="[innerListStyle]">
+											<template v-if="finalUseVirtualList">
+												<view class="zp-list-cell" :style="[innerCellStyle]" :id="`${fianlVirtualCellIdPrefix}-${item[virtualCellIndexKey]}`" v-for="(item,index) in virtualList" :key="item['zp_unique_index']" @click="_innerCellClick(item,virtualTopRangeIndex+index)">
+													<view v-if="useCompatibilityMode">使用兼容模式请在组件源码z-paging.vue第105行中注释这一行,并打开下面一行注释</view>
+													<!-- <zp-public-virtual-cell v-if="useCompatibilityMode" :extraData="extraData" :item="item" :index="virtualTopRangeIndex+index" /> -->
+													<slot v-else name="cell" :item="item" :index="virtualTopRangeIndex+index"/>
+												</view>
+											</template>
+											<template v-else>
+												<view class="zp-list-cell" v-for="(item,index) in realTotalData" :key="index" @click="_innerCellClick(item,index)">
+													<slot name="cell" :item="item" :index="index"/>
+												</view>
+											</template>
+										</view>
+										<slot name="footer"/>
+									</template>
+									<!-- 聊天记录模式加载更多loading -->
+									<template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&(realTotalData.length||(showChatLoadingWhenReload&&showLoading))&&!isFirstPageAndNoMore">
+										<view :style="[chatRecordRotateStyle]">
+											<slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
+											<template v-else>
+												<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
+												<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
+											</template>
+										</view>
+									</template>
+									<!-- 虚拟列表底部占位view -->
+									<view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderBottomHeight+'px'}]"/>
+									<!-- 上拉加载更多view -->
+									<!-- #ifndef MP-ALIPAY -->
+									<slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
+									<slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
+									<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
+									<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
+									<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
+									<!-- #endif -->
+									<!-- #ifdef MP-ALIPAY -->
+									<slot v-if="loadingStatus===M.Default&&zSlots.loadingMoreDefault&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreDefault" />
+									<slot v-else-if="loadingStatus===M.Loading&&zSlots.loadingMoreLoading&&showLoadingMore&&loadingMoreEnabled" name="loadingMoreLoading" />
+									<slot v-else-if="loadingStatus===M.NoMore&&zSlots.loadingMoreNoMore&&showLoadingMore&&showLoadingMoreNoMoreView&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreNoMore" />
+									<slot v-else-if="loadingStatus===M.Fail&&zSlots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreFail" />
+									<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===M.NoMore&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :zConfig="zLoadMoreConfig" />
+									<!-- #endif -->
+									<!-- 底部安全区域useSafeAreaPlaceholder模式占位,此时占位不再固定在底部而是跟随页面一起滚动 -->
+									<!-- 如果底部slot=bottom存在,占位区域会插入在slot=bottom下方,不再跟随页面滚动,因此这里就没必要显示了 -->
+									<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+									<view v-if="safeAreaInsetBottom&&finalUseSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+								</view>
+								<!-- 空数据图 -->
+								<view v-if="showEmpty" :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}" :style="[emptyViewSuperStyle,chatRecordRotateStyle]">
+									<slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed"/>
+									<z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload" 
+									:emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle" 
+									:emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit" 
+									@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
+								</view>
+							</view>
+						</view>
+					</view>
+				</scroll-view>
+			</view>
+			<view v-if="zSlots.right" :class="{'zp-page-right':true,'zp-absoulte zp-right':finalIsOldWebView}">
+				<slot name="right" />
+			</view>
+		</view>
+		<!-- 底部固定的slot -->
+		<view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
+			<template v-if="zSlots.bottom">
+				<!-- 非页面滚动底部插槽(父容器开启flex,中间列表设置了flex:1,通过中间列表撑开固定在底部) -->
+				<slot v-if="!usePageScroll" name="bottom" />
+				<!-- 页面滚动底部插槽(通过position: fixed固定在底部) -->
+				<view v-else class="zp-page-bottom" @touchmove.stop.prevent :style="[{'bottom': `${windowBottom}px`, 'background': bottomBgColor}]">
+					<slot name="bottom" />
+					<!-- 页面滚动底部安全区域占位(仅slot=bottom存在时展示在slot=bottom插入的view下方,当slot=bottom不存在时,通过控制容器的marginBottom设置底部安全区域间距) -->
+					<view v-if="safeAreaInsetBottom" :style="[{height:safeAreaBottom+'px'}]" />
+				</view>
+			</template>
+			<!-- 非页面滚动底部安全区域占位(无论slot=bottom是否存在)-->
+			<!-- 如果useSafeAreaPlaceholder开启了并且slot=bottom不存在就不显示这个占位view了,因为此时useSafeAreaPlaceholder会是跟随滚动的状态 -->
+			<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+			<view v-if="safeAreaInsetBottom&&!usePageScroll&&!(finalUseSafeAreaPlaceholder)&&!useChatRecordMode" :style="[{height:safeAreaBottom+'px'}]" />
+			
+			<!-- 聊天记录模式底部占位 -->
+			<template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
+				<view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
+				<view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
+			</template>
+		</view>
+		<!-- 点击返回顶部view -->
+		<view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
+			<slot v-if="zSlots.backToTop" name="backToTop" />
+			<image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
+		</view>
+		<!-- 全屏Loading(铺满z-paging并固定) -->
+		<view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
+			<slot name="loading" />
+		</view>
+	</view>
+	<!-- #endif -->
+	<!-- #ifdef APP-NVUE -->
+	<component ref="z-paging-content" :is="finalNvueSuperListIs" :style="[finalPagingStyle]" :class="[{'z-paging-content-fixed':fixed&&!usePageScroll}, pagingClass]" :scrollable="false">
+		<!-- 二楼view -->
+		<view v-if="showF2 && showRefresherF2" ref="zp-n-f2" class="zp-f2-content" @touchmove.stop.prevent :style="[{'height': superContentHeight + 'px', 'width': nRefresherWidth + 'px', 'opacity': nF2Opacity}]">
+			<slot name="f2"/>
+		</view>
+		<!-- 顶部固定的slot -->
+		<view ref="zp-page-top" v-if="zSlots.top" :class="{'zp-page-top':usePageScroll}" :style="[usePageScroll?{'top':`${windowTop}px`,'z-index':topZIndex}:{}]">
+			<slot name="top" />
+		</view>
+		<!-- 聊天记录模式加载更多loading(loading时候显示) -->
+		<view v-if="useChatRecordMode&&loadingStatus!==M.NoMore&&showChatLoadingWhenReload&&showLoading">
+			<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
+			<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
+		</view>
+		<component :is="finalNvueSuperListIs" class="zp-n-list-container" :scrollable="false">
+			<view v-if="zSlots.left" class="zp-page-left">
+				<slot name="left" />
+			</view>
+			<!-- 因在nvue+vue3+waterfall中,使用<component is="waterfall" />设置的瀑布流无效,因此此处只能单独判断finalNvueListIs等于waterfall时,直接写<waterfall />标签暂时解决 -->
+			<!-- 下方的v-if和v-else中的代码完全一致,仅标签不同,等待官方解决后再统一,已提issue:https://ask.dcloud.net.cn/question/168505 -->
+			<component v-if="finalNvueListIs !== 'waterfall'" :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},chatRecordRotateStyle]" :alwaysScrollableVertical="true"
+				:fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar" :loadmoreoffset="finalLowerThreshold" :enable-back-to-top="enableBackToTop"
+				:scrollable="finalScrollable" :bounce="nvueBounce" :column-count="nWaterfallColumnCount" :column-width="nWaterfallColumnWidth"
+				:column-gap="nWaterfallColumnGap" :left-gap="nWaterfallLeftGap" :right-gap="nWaterfallRightGap" :pagingEnabled="nvuePagingEnabled" :offset-accuracy="offsetAccuracy"
+				@loadmore="_nOnLoadmore" @scroll="_nOnScroll" @scrollend="_nOnScrollend">
+				<refresh v-if="(zSlots.top?cacheTopHeight!==-1:true)&&finalNvueRefresherEnabled" class="zp-n-refresh" :style="[nvueRefresherStyle]" :display="nRefresherLoading?'show':'hide'" @refresh="_nOnRrefresh" @pullingdown="_nOnPullingdown">
+					<view ref="zp-n-refresh-container" class="zp-n-refresh-container" :style="[{background:refresherBackground,width:nRefresherWidth}]" id="zp-n-refresh-container">
+						<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
+						<!-- 下拉刷新view -->
+						<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+						<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
+						<slot v-else-if="(nScopedSlots?nScopedSlots:zSlots).refresher" :refresherStatus="refresherStatus" name="refresher" />
+						<z-paging-refresh ref="refresh" v-else :status="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
+							:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
+							:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
+							:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+							:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
+					</view>
+				</refresh>
+				<component :is="nViewIs" v-if="isIos&&!useChatRecordMode?oldScrollTop>10:true" ref="zp-n-list-top-tag" class="zp-n-list-top-tag" style="margin-top: -1rpx;" :style="[{height:finalNvueRefresherEnabled?'0px':'1px'}]"></component>
+				<component :is="nViewIs" v-if="nShowRefresherReveal" ref="zp-n-list-refresher-reveal" :style="[{transform:`translateY(-${nShowRefresherRevealHeight}px)`},{background:refresherBackground}]">
+					<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
+					<!-- 下拉刷新view -->
+					<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+					<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
+					<slot v-else-if="(nScopedSlots?nScopedSlots:$slots).refresher" :refresherStatus="R.Loading" name="refresher" />
+					<z-paging-refresh ref="refresh" v-else :status="R.Loading" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
+						:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
+						:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
+						:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+						:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
+				</component>
+				<!-- 内置列表 -->
+				<template v-if="finalUseInnerList">
+					<component :is="nViewIs">
+						<slot name="header"/>
+					</component>	
+					<component :is="nViewIs" class="zp-list-cell" v-for="(item,index) in realTotalData" :key="finalCellKeyName.length?item[finalCellKeyName]:index">
+						<slot name="cell" :item="item" :index="index"/>
+					</component>
+					<component :is="nViewIs">
+						<slot name="footer"/>
+					</component>	
+				</template>
+				<template v-else>
+					<slot />
+				</template>
+				<!-- 全屏Loading -->
+				<component :is="nViewIs" v-if="showLoading&&zSlots.loading&&!loadingFullFixed" :class="{'z-paging-content-fixed':usePageScroll}" style="flex:1" :style="[chatRecordRotateStyle]">
+					<slot name="loading" />
+				</component>
+				<!-- 上拉加载更多view -->
+				<component :is="nViewIs" v-if="!isOnly&&loadingMoreEnabled&&!showEmpty">
+					<!-- 聊天记录模式加载更多loading(滚动到顶部加载更多或无更多数据时显示) -->
+					<template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&realTotalData.length&&isChatRecordModeAndInversion">
+						<view :style="[chatRecordRotateStyle]">
+							<slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
+							<template v-else>
+								<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
+								<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
+							</template>
+						</view>
+					</template>
+					
+					<view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:loadingMoreFixedHeight}:{}">
+						<slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
+						<slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
+						<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
+						<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
+						<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
+						<!-- 底部安全区域useSafeAreaPlaceholder模式占位,此时占位不再固定在底部而是跟随页面一起滚动 -->
+						<!-- 如果底部slot=bottom存在,占位区域会插入在slot=bottom下方,不再跟随页面滚动,因此这里就没必要显示了 -->
+						<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+						<view v-if="safeAreaInsetBottom&&finalUseSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+					</view>
+				</component>
+				<!-- 空数据图 -->
+				<component :is="nViewIs" v-if="showEmpty" :class="{'z-paging-content-fixed':usePageScroll}" :style="[{flex:emptyViewCenter?1:0},emptyViewSuperStyle,chatRecordRotateStyle]">
+					<view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}">
+						<slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed" />
+						<z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload" 
+						:emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle" 
+						:emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
+						@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
+					</view>
+				</component>
+				<component :is="nViewIs" v-if="!hideNvueBottomTag" ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag"></component>
+			</component>
+			<waterfall v-else :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},chatRecordRotateStyle]" :alwaysScrollableVertical="true"
+				:fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar" :loadmoreoffset="finalLowerThreshold" :enable-back-to-top="enableBackToTop"
+				:scrollable="finalScrollable" :bounce="nvueBounce" :column-count="nWaterfallColumnCount" :column-width="nWaterfallColumnWidth"
+				:column-gap="nWaterfallColumnGap" :left-gap="nWaterfallLeftGap" :right-gap="nWaterfallRightGap" :pagingEnabled="nvuePagingEnabled" :offset-accuracy="offsetAccuracy"
+				@loadmore="_nOnLoadmore" @scroll="_nOnScroll" @scrollend="_nOnScrollend">
+				<refresh v-if="(zSlots.top?cacheTopHeight!==-1:true)&&finalNvueRefresherEnabled" class="zp-n-refresh" :style="[nvueRefresherStyle]" :display="nRefresherLoading?'show':'hide'" @refresh="_nOnRrefresh" @pullingdown="_nOnPullingdown">
+					<view ref="zp-n-refresh-container" class="zp-n-refresh-container" :style="[{background:refresherBackground,width:nRefresherWidth}]" id="zp-n-refresh-container">
+						<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
+						<!-- 下拉刷新view -->
+						<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+						<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
+						<slot v-else-if="(nScopedSlots?nScopedSlots:zSlots).refresher" :refresherStatus="refresherStatus" name="refresher" />
+						<z-paging-refresh ref="refresh" v-else :status="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
+							:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
+							:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
+							:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+							:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
+					</view>
+				</refresh>
+				<component :is="nViewIs" v-if="isIos&&!useChatRecordMode?oldScrollTop>10:true" ref="zp-n-list-top-tag" class="zp-n-list-top-tag" style="margin-top: -1rpx;" :style="[{height:finalNvueRefresherEnabled?'0px':'1px'}]"></component>
+				<component :is="nViewIs" v-if="nShowRefresherReveal" ref="zp-n-list-refresher-reveal" :style="[{transform:`translateY(-${nShowRefresherRevealHeight}px)`},{background:refresherBackground}]">
+					<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
+					<!-- 下拉刷新view -->
+					<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
+					<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
+					<slot v-else-if="(nScopedSlots?nScopedSlots:$slots).refresher" :refresherStatus="R.Loading" name="refresher" />
+					<z-paging-refresh ref="refresh" v-else :status="R.Loading" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
+						:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
+						:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
+						:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
+						:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
+				</component>
+				<!-- 内置列表 -->
+				<template v-if="finalUseInnerList">
+					<component :is="nViewIs">
+						<slot name="header"/>
+					</component>	
+					<component :is="nViewIs" class="zp-list-cell" v-for="(item,index) in realTotalData" :key="finalCellKeyName.length?item[finalCellKeyName]:index">
+						<slot name="cell" :item="item" :index="index"/>
+					</component>
+					<component :is="nViewIs">
+						<slot name="footer"/>
+					</component>	
+				</template>
+				<template v-else>
+					<slot />
+				</template>
+				<!-- 全屏Loading -->
+				<component :is="nViewIs" v-if="showLoading&&zSlots.loading&&!loadingFullFixed" :class="{'z-paging-content-fixed':usePageScroll}" style="flex:1" :style="[chatRecordRotateStyle]">
+					<slot name="loading" />
+				</component>
+				<!-- 上拉加载更多view -->
+				<component :is="nViewIs" v-if="!isOnly&&loadingMoreEnabled&&!showEmpty">
+					<!-- 聊天记录模式加载更多loading(滚动到顶部加载更多或无更多数据时显示) -->
+					<template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&realTotalData.length&&isChatRecordModeAndInversion">
+						<view :style="[chatRecordRotateStyle]">
+							<slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
+							<template v-else>
+								<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
+								<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
+							</template>
+						</view>
+					</template>
+					
+					<view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:loadingMoreFixedHeight}:{}">
+						<slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
+						<slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
+						<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
+						<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
+						<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
+						<!-- 底部安全区域useSafeAreaPlaceholder模式占位,此时占位不再固定在底部而是跟随页面一起滚动 -->
+						<!-- 如果底部slot=bottom存在,占位区域会插入在slot=bottom下方,不再跟随页面滚动,因此这里就没必要显示了 -->
+						<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+						<view v-if="safeAreaInsetBottom&&finalUseSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+					</view>
+				</component>
+				<!-- 空数据图 -->
+				<component :is="nViewIs" v-if="showEmpty" :class="{'z-paging-content-fixed':usePageScroll}" :style="[{flex:emptyViewCenter?1:0},emptyViewSuperStyle,chatRecordRotateStyle]">
+					<view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}">
+						<slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed" />
+						<z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload" 
+						:emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle" 
+						:emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
+						@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
+					</view>
+				</component>
+				<component :is="nViewIs" v-if="!hideNvueBottomTag" ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag"></component>
+			</waterfall>
+			<view v-if="zSlots.right" class="zp-page-right">
+				<slot name="right" />
+			</view>
+		</component>
+		<!-- 底部固定的slot -->
+		<view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
+			<slot name="bottom" />
+			<!-- 非页面滚动底部安全区域占位(无论slot=bottom是否存在)-->
+			<!-- 如果useSafeAreaPlaceholder开启了并且slot=bottom不存在就不显示这个占位view了,因为此时useSafeAreaPlaceholder会是跟随滚动的状态 -->
+			<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+			<view v-if="safeAreaInsetBottom&&!usePageScroll&&!(finalUseSafeAreaPlaceholder)&&!useChatRecordMode" :style="[{height:safeAreaBottom+'px'}]" />
+			
+			<!-- 聊天记录模式底部占位 -->
+			<template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
+				<view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
+				<view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
+			</template>
+		</view>
+		<!-- 点击返回顶部view -->
+		<view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
+			<slot v-if="zSlots.backToTop" name="backToTop" />
+			<image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
+		</view>
+		<!-- 全屏Loading(铺满z-paging并固定) -->
+		<view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
+			<slot name="loading" />
+		</view>
+	</component>
+	<!-- #endif -->
+</template>
+<script module="pagingRenderjs" lang="renderjs">
+	import pagingRenderjs from './wxs/z-paging-renderjs.js';
+	/**
+	 * z-paging 分页组件
+	 * @description z-paging 分页组件,高性能,全平台兼容。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、全自动分页、无闪动聊天分页、本地分页等,也支持作为基本布局容器使用
+	 * @tutorial https://z-paging.zxlee.cn
+	 * @property {Array} value 父组件v-model所绑定的list的值,默认为[]
+	 * @property {Number|String} defaultPageNo 自定义初始的pageNo,默认为1
+	 * @property {Number|String} defaultPageSize 自定义pageSize(每页显示多少条),默认为10
+	 * @property {Boolean} fixed z-paging是否使用fixed布局,默认为true
+	 * @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认为false
+	 * @property {Boolean} useSafeAreaPlaceholder 开启底部安全区域适配后,是否使用placeholder形式实现,默认为false
+	 * @property {Boolean} usePageScroll 使用页面滚动,默认为false
+	 * @property {Boolean} autoFullHeight 使用页面滚动时,是否在不满屏时自动填充满屏幕,默认为true
+	 * @property {String} defaultThemeStyle loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认为black
+	 * @property {Object} pagingStyle 设置z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
+	 * @property {String|Array|Object} pagingClass 设置z-paging的class,优先级低于pagingStyle和height、width、maxWidth、bgColor
+	 * @property {String} height z-paging的高度,优先级低于pagingStyle中设置的height,传字符串,如100px、100rpx、100%
+	 * @property {String} width z-paging的宽度,优先级低于pagingStyle中设置的width,传字符串,如100px、100rpx、100%
+	 * @property {String} maxWidth z-paging的最大宽度,优先级低于pagingStyle中设置的max-width,默认为空
+	 * @property {String} bgColor z-paging的背景色(为css中的background,因此也可以设置渐变,背景图片等),优先级低于pagingStyle中设置的background-color
+	 * @property {Boolean} watchTouchDirectionChange 是否监听列表触摸方向改变,默认为false
+	 * @property {Boolean} watchScrollDirectionChange 是否监听列表滚动方向改变,默认为false
+	 * @property {Boolean} layoutOnly 是否只使用基础布局,设置为true后将关闭mounted自动请求数据、关闭下拉刷新和滚动到底部加载更多,强制隐藏空数据图。默认为否
+	 * @property {Number|String} delay 调用complete后延迟处理的时间,单位为毫秒,优先级高于min-delay,默认为0
+	 * @property {Number|String} minDelay 触发@query后最小延迟处理的时间,单位为毫秒,优先级低于delay,默认为0
+	 * @property {Boolean} callNetworkReject 请求失败是否触发reject,默认为true
+	 * @property {String} unit z-paging中默认布局的单位,默认为rpx
+	 * @property {Boolean} concat 自动拼接complete中传过来的数组,默认为true
+	 * @property {Number|String|Object} dataKey 为保证数据一致,设置当前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
+	 * @property {String} autowireListName 【极简写法】自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
+	 * @property {String} autowireQueryName 【极简写法】自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
+	 * @property {Function} fetch 【极简写法】获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
+	 * @property {Object} fetchParams fetch的附加参数,fetch配置后有效
+	 * @property {Boolean} auto [z-paging]mounted后是否自动调用reload方法(mounted后自动调用接口),默认为true
+	 * @property {Boolean} autoScrollToTopWhenReload reload时自动滚动到顶部,默认为true
+	 * @property {Boolean} autoCleanListWhenReload reload时立即自动清空原list,默认为true
+	 * @property {Boolean} showRefresherWhenReload 列表刷新时自动显示下拉刷新view,默认为false
+	 * @property {Boolean} showLoadingMoreWhenReload 列表刷新时自动显示加载更多view,且为加载中状态,默认为false
+	 * @property {Boolean} createdReload 组件created时立即触发reload,默认为false
+	 * @property {Boolean} refresherEnabled 是否开启下拉刷新,默认为true
+	 * @property {Number|String} refresherThreshold 设置自定义下拉刷新阈值,默认单位为px,默认为80rpx
+	 * @property {Boolean} useRefresherStatusBarPlaceholder 是否开启下拉刷新状态栏占位,默认为false
+	 * @property {Boolean} refresherOnly 是否只使用下拉刷新,默认为false
+	 * @property {Boolean} useCustomRefresher 是否使用自定义的下拉刷新,默认为true
+	 * @property {Boolean} reloadWhenRefresh 用户下拉刷新时是否触发reload方法,默认为true
+	 * @property {String} refresherThemeStyle 下拉刷新的主题样式,支持black,white,默认为black
+	 * @property {Object} refresherImgStyle 自定义下拉刷新中左侧图标的样式
+	 * @property {Object} refresherTitleStyle 自定义下拉刷新中右侧状态描述文字的样式
+	 * @property {Object} refresherUpdateTimeStyle 自定义下拉刷新中右侧最后更新时间文字的样式
+	 * @property {Boolean} watchRefresherTouchmove 是否实时监听下拉刷新中进度,并通过@refresherTouchmove传递给父组件,默认为false
+	 * @property {Boolean} showRefresherUpdateTime 是否显示最后更新时间,默认为false
+	 * @property {String|Object} refresherDefaultText 自定义下拉刷新默认状态下的文字
+	 * @property {String|Object} refresherPullingText 自定义下拉刷新松手立即刷新状态下的文字
+	 * @property {String|Object} refresherRefreshingText 自定义下拉刷新刷新中状态下的文字
+	 * @property {String|Object} refresherCompleteText 自定义下拉刷新刷新结束状态下的文字
+	 * @property {String} refresherDefaultImg 自定义下拉刷新默认状态下的图片
+	 * @property {String} refresherPullingImg 自定义下拉刷新松手立即刷新状态下的图片
+	 * @property {String} refresherRefreshingImg 自定义下拉刷新刷新中状态下的图片
+	 * @property {String} refresherCompleteImg 自定义下拉刷新刷新结束状态下的图片
+	 * @property {Boolean} refresherRefreshingAnimated 自定义下拉刷新刷新中状态下是否展示旋转动画,默认为true
+	 * @property {Boolean} refresherEndBounceEnabled 是否开启自定义下拉刷新刷新结束回弹动画效果,默认为true
+	 * @property {String} refresherDefaultStyle 设置系统下拉刷新默认样式,支持设置black,white,none,默认为black
+	 * @property {String} refresherBackground 设置自定义下拉刷新区域背景颜色,默认为#FFFFFF00
+	 * @property {String} refresherFixedBackground 设置固定的自定义下拉刷新区域背景颜色,默认为#FFFFFF00
+	 * @property {Number|String} refresherFixedBacHeight 设置固定的自定义下拉刷新区域高度,默认为0
+	 * @property {Number|String} refresherDefaultDuration 设置自定义下拉刷新默认状态下回弹动画时间,单位为毫秒,默认为100
+	 * @property {Number|String} refresherCompleteDelay 自定义下拉刷新结束以后延迟收回的时间,单位为毫秒,默认为0
+	 * @property {Number|String} refresherCompleteDuration 自定义下拉刷新结束收回动画时间,单位为毫秒,默认为300
+	 * @property {Boolean} refresherVibrate 下拉刷新时下拉到“松手立即刷新”状态时是否使手机短振动,默认为false
+	 * @property {Boolean} refresherRefreshingScrollable 自定义下拉刷新刷新中状态是否允许列表滚动,默认为true
+	 * @property {Boolean} refresherCompleteScrollable 自定义下拉刷新结束状态下是否允许列表滚动,默认为false
+	 * @property {Number} refresherOutRate 设置自定义下拉刷新下拉超出阈值后继续下拉位移衰减的比例,默认为0.65
+	 * @property {Boolean} refresherF2Enabled 是否开启下拉进入二楼功能,默认为false
+	 * @property {Number|String} refresherF2Threshold 下拉进入二楼阈值,默认为200rpx
+	 * @property {Number|String} refresherF2Duration 下拉进入二楼动画时间,单位为毫秒,默认为200
+	 * @property {Boolean} showRefresherF2 下拉进入二楼状态松手后是否弹出二楼,默认为true
+	 * @property {Number} refresherPullRate 设置自定义下拉刷新下拉时实际下拉位移与用户下拉距离的比值,默认为0.75
+	 * @property {Number|String} refresherFps 自定义下拉刷新下拉帧率,默认为40
+	 * @property {Number|String} refresherMaxAngle 自定义下拉刷新允许触发的最大下拉角度,默认为40度
+	 * @property {Boolean} refresherAngleEnableChangeContinued 自定义下拉刷新的角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为false
+	 * @property {Boolean} refresherNoTransform 下拉刷新时是否禁止下拉刷新view跟随用户触摸竖直移动,默认为false
+	 * @property {Boolean} loadingMoreEnabled 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为true
+	 * @property {Number|String} lowerThreshold 距底部/右边多远时,触发scrolltolower事件,默认单位为px,默认为100rpx
+	 * @property {Boolean} toBottomLoadingMoreEnabled 是否启用滑动到底部加载更多数据,默认为true
+	 * @property {String} loadingMoreThemeStyle 底部加载更多的主题样式,支持black,white,默认为black
+	 * @property {Object} loadingMoreCustomStyle 自定义底部加载更多样式
+	 * @property {Object} loadingMoreTitleCustomStyle 自定义底部加载更多文字样式
+	 * @property {Object} loadingMoreLoadingIconCustomStyle 自定义底部加载更多加载中动画样式
+	 * @property {String} loadingMoreLoadingIconType 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
+	 * @property {String} loadingMoreLoadingIconCustomImage 自定义底部加载更多加载中动画图标图片
+	 * @property {Boolean} loadingMoreLoadingAnimated 底部加载更多加载中view是否展示旋转动画,默认为true
+	 * @property {String|Object} loadingMoreDefaultText 滑动到底部"默认"文字
+	 * @property {String|Object} loadingMoreLoadingText 滑动到底部"加载中"文字
+	 * @property {String|Object} loadingMoreNoMoreText 滑动到底部"没有更多"文字
+	 * @property {String|Object} loadingMoreFailText 滑动到底部"加载失败"文字
+	 * @property {Boolean} hideNoMoreInside 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为false
+	 * @property {Number} hideNoMoreByLimit 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0
+	 * @property {Boolean} insideMore 当分页未满一屏时,是否自动加载更多,默认为false
+	 * @property {Boolean} loadingMoreDefaultAsLoading 滑动到底部状态为默认状态时,以加载中的状态展示,默认为false
+	 * @property {Boolean} showLoadingMoreNoMoreView 是否显示没有更多数据的view,默认为true
+	 * @property {Boolean} showDefaultLoadingMoreText 是否显示默认的加载更多text,默认为true
+	 * @property {Boolean} showLoadingMoreNoMoreLine 是否显示没有更多数据的分割线,默认为true
+	 * @property {Object} loadingMoreNoMoreLineCustomStyle 自定义底部没有更多数据的分割线样式
+	 * @property {Boolean} hideEmptyView 是否强制隐藏空数据图,默认为false
+	 * @property {Boolean} emptyViewFixed 空数据图片是否铺满z-paging,默认为false
+	 * @property {Boolean} emptyViewCenter 空数据图片是否垂直居中,默认为true
+	 * @property {String|Object} emptyViewText 空数据图描述文字
+	 * @property {String} emptyViewImg 空数据图图片
+	 * @property {String} emptyViewErrorImg 空数据图“加载失败”图片
+	 * @property {String|Object} emptyViewReloadText 空数据图点击重新加载文字
+	 * @property {String|Object} emptyViewErrorText 空数据图“加载失败”描述文字
+	 * @property {Object} emptyViewSuperStyle 空数据图父view样式
+	 * @property {Object} emptyViewStyle 空数据图样式
+	 * @property {Object} emptyViewImgStyle 空数据图img样式
+	 * @property {Object} emptyViewTitleStyle 空数据图描述文字样式
+	 * @property {Object} emptyViewReloadStyle 空数据图重新加载按钮样式
+	 * @property {Boolean} showEmptyViewReload 是否显示空数据图重新加载按钮(无数据时),默认为false
+	 * @property {Boolean} showEmptyViewReloadWhenError 加载失败时是否显示空数据图重新加载按钮,默认为true
+	 * @property {Boolean} autoHideEmptyViewWhenLoading 加载中时是否自动隐藏空数据图,默认为true
+	 * @property {Boolean} autoHideEmptyViewWhenPull 用户下拉列表触发下拉刷新加载中时是否自动隐藏空数据图,默认为true
+	 * @property {Boolean} autoHideLoadingAfterFirstLoaded 第一次加载后自动隐藏loading slot,默认为true
+	 * @property {Boolean} loadingFullFixed loading slot的父view是否铺满屏幕并固定,默认为false
+	 * @property {Boolean} autoShowSystemLoading 是否自动显示系统Loading:即uni.showLoading,默认为false
+	 * @property {String|Object} systemLoadingText 显示系统Loading时显示的文字
+	 * @property {Boolean} systemLoadingMask 显示系统Loading时是否显示透明蒙层,防止触摸穿透,默认为true
+	 * @property {Boolean} autoShowBackToTop 自动显示点击返回顶部按钮,默认为false
+	 * @property {Number|String} backToTopThreshold 点击返回顶部按钮显示/隐藏的阈值(滚动距离),默认单位为px,默认为400rpx
+	 * @property {String} backToTopImg 点击返回顶部按钮的自定义图片地址
+	 * @property {Boolean} backToTopWithAnimate 点击返回顶部按钮返回到顶部时是否展示过渡动画,默认为true
+	 * @property {Number|String} backToTopBottom 点击返回顶部按钮与底部的距离,默认单位为px,默认为160rpx
+	 * @property {Object} backToTopStyle 点击返回顶部按钮的自定义样式
+	 * @property {Boolean} useVirtualList 是否使用虚拟列表,默认为false
+	 * @property {Boolean} useCompatibilityMode 在使用虚拟列表时,是否使用兼容模式,默认为false
+	 * @property {Object} extraData 使用兼容模式时传递的附加数据
+	 * @property {String} cellHeightMode 虚拟列表cell高度模式,默认为fixed
+	 * @property {Number|String} preloadPage 预加载的列表可视范围(列表高度)页数,默认为12
+	 * @property {Number|String} fixedCellHeight 固定的cell高度,`cell-height-mode=fixed`才有效,默认为空
+	 * @property {Number|String} virtualListCol 虚拟列表列数,默认为1
+	 * @property {Number|String} virtualScrollFps 虚拟列表scroll取样帧率,默认为80
+	 * @property {String} virtualCellIdPrefix 虚拟列表cell id的前缀
+	 * @property {Boolean} useInnerList 是否在z-paging内部循环渲染列表(使用内置列表),默认为false
+	 * @property {Boolean} forceCloseInnerList 强制关闭inner-list,默认为false
+	 * @property {Boolean} virtualInSwiperSlot 虚拟列表是否使用swiper-item包裹,默认为false
+	 * @property {String} cellKeyName 内置列表cell的key名称(仅nvue有效)
+	 * @property {Object} innerListStyle innerList样式
+	 * @property {Object} innerCellStyle innerCell样式
+	 * @property {Number|String} localPagingLoadingTime 本地分页时上拉加载更多延迟时间,单位为毫秒,默认为200
+	 * @property {Boolean} useChatRecordMode 使用聊天记录模式,默认为false
+	 * @property {Boolean} autoHideKeyboardWhenChat 使用聊天记录模式时是否自动隐藏键盘,默认为true
+	 * @property {Boolean} autoAdjustPositionWhenChat 使用聊天记录模式中键盘弹出时是否自动调整slot="bottom"高度,默认为true
+	 * @property {Boolean} autoToBottomWhenChat 使用聊天记录模式中键盘弹出时是否自动滚动到底部,默认为false
+	 * @property {String} chatAdjustPositionOffset 使用聊天记录模式中键盘弹出时占位高度偏移距离,默认为0px
+	 * @property {Boolean} showChatLoadingWhenReload 使用聊天记录模式中`reload`时是否显示`chatLoading`,默认为false
+	 * @property {String} bottomBgColor `bottom`的背景色,默认透明
+	 * @property {Boolean} chatLoadingMoreDefaultAsLoading 在聊天记录模式中滑动到顶部状态为默认状态时,是否以加载中的状态展示,默认为true
+	 * @property {Boolean} showScrollbar 控制是否出现滚动条,默认为true
+	 * @property {Boolean} scrollable 是否可以滚动,使用内置scroll-view和nvue时有效,默认为true
+	 * @property {Boolean} scrollX 是否允许横向滚动,默认为false
+	 * @property {Boolean} scrollToTopBounceEnabled iOS设备上滚动到顶部时是否允许回弹效果,默认为false
+	 * @property {Boolean} scrollToBottomBounceEnabled iOS设备上滚动到底部时是否允许回弹效果,默认为true
+	 * @property {Boolean} scrollWithAnimation 在设置滚动条位置时使用动画过渡,默认为false
+	 * @property {String} scrollIntoView 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
+	 * @property {Boolean} enableBackToTop iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,默认为true
+	 * @property {String} nvueListIs nvue中修改列表类型,默认为list
+	 * @property {Object} nvueWaterfallConfig waterfall配置,仅在nvue中且nvueListIs=waterfall时有效
+	 * @property {Boolean} nvueBounce nvue控制是否回弹效果,iOS不支持动态修改,默认为true
+	 * @property {Boolean} nvueFastScroll nvue中通过代码滚动到顶部/底部时,是否加快动画效果,默认为false
+	 * @property {String} nvueListId nvue中list的id
+	 * @property {Boolean} hideNvueBottomTag 是否隐藏nvue列表底部的tagView,默认为false
+	 * @property {Boolean} nvuePagingEnabled 设置nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
+	 * @property {Number} offsetAccuracy nvue中控制onscroll事件触发的频率,默认为空
+	 * @property {Boolean} useCache 是否使用缓存,默认为false
+	 * @property {String} cacheKey 使用缓存时缓存的key
+	 * @property {String} cacheMode 缓存模式,默认为default
+	 * @property {Number} topZIndex slot="top"的view的z-index,默认为99
+	 * @property {Number} superContentZIndex z-paging内容容器父view的z-index,默认为1
+	 * @property {Number} contentZIndex z-paging内容容器部分的z-index,默认为1
+	 * @property {Number} emptyViewZIndex 空数据view的z-index,默认为9
+	 * @property {Boolean} autoHeight z-paging是否自动高度,默认为false
+	 * @property {Number|String} autoHeightAddition z-paging自动高度时的附加高度,默认为0px
+	 * @event {Function} input 父组件v-model所绑定的list的值改变时触发此事件
+	 * @event {Function} query 下拉刷新或滚动到底部时会自动触发此方法。z-paging加载时也会触发(若要禁止,请设置:auto="false")。pageNo和pageSize会自动计算好,直接传给服务器即可。
+	 * @event {Function} listChange 分页渲染的数组改变时触发
+	 * @event {Function} refresherStatusChange 自定义下拉刷新状态改变
+	 * @event {Function} refresherTouchstart 自定义下拉刷新下拉开始
+	 * @event {Function} refresherTouchmove 自定义下拉刷新下拉拖动中
+	 * @event {Function} refresherTouchend 自定义下拉刷新下拉结束
+	 * @event {Function} refresherF2Change 下拉进入二楼状态改变
+	 * @event {Function} refresh 自定义下拉刷新被触发
+	 * @event {Function} restore 自定义下拉刷新被复位
+	 * @event {Function} loadingStatusChange 自定义下拉刷新状态改变
+	 * @event {Function} emptyViewReload 点击了空数据图中的重新加载按钮
+	 * @event {Function} emptyViewClick 点击了空数据图view
+	 * @event {Function} isLoadFailedChange z-paging请求失败状态改变
+	 * @event {Function} backToTopClick 点击了返回顶部按钮
+	 * @event {Function} virtualListChange 虚拟列表当前渲染的数组改变时触发
+	 * @event {Function} innerCellClick 使用虚拟列表或内置列表时点击了cell
+	 * @event {Function} virtualPlaceholderTopHeight 虚拟列表顶部占位高度改变
+	 * @event {Function} hidedKeyboard 在聊天记录模式下,触摸列表隐藏了键盘
+	 * @event {Function} keyboardHeightChange 键盘高度改变
+	 * @event {Function} scroll z-paging列表滚动时触发
+	 * @event {Function} scrollTopChange scrollTop改变时触发
+	 * @event {Function} scrolltolower z-paging内置的scroll-view/list-view/waterfall滚动底部时触发
+	 * @event {Function} scrolltoupper z-paging内置的scroll-view/list-view/waterfall滚动顶部时触发
+	 * @event {Function} scrollend z-paging内置的list滚动结束时触发
+	 * @event {Function} contentHeightChanged z-paging中内容高度改变时触发
+	 * @event {Function} touchDirectionChange 监听列表触摸方向改变(nvue无效)
+	 * @event {Function} scrollDirectionChange 监听列表滚动方向改变(页面滚动无效)
+	 * @example <z-paging ref="paging" v-model="dataList" @query="queryList"></z-paging>
+	 */
+	export default {
+		name:"z-paging",
+		// #ifdef APP-VUE || H5
+		mixins: [pagingRenderjs],
+		// #endif
+	}
+</script>
+
+<script src="./js/z-paging-main.js" />
+<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
+<script src="./wxs/z-paging-wxs.wxs" module="pagingWxs" lang="wxs"></script>
+<!-- #endif -->
+
+	
+<style scoped>
+	@import "./css/z-paging-main.css";
+	@import "./css/z-paging-static.css";
+</style>

+ 89 - 0
uni_modules/z-paging/package.json

@@ -0,0 +1,89 @@
+{
+  "id": "z-paging",
+  "name": "z-paging",
+  "displayName": "【z-paging下拉刷新、上拉加载】高性能,全平台兼容。支持虚拟列表,分页全自动处理",
+  "version": "2.8.7",
+  "description": "超简单、低耦合!使用wxs+renderjs实现。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、无闪动聊天分页、本地分页、国际化等数百项配置",
+  "keywords": [
+    "下拉刷新",
+    "上拉加载",
+    "分页器",
+    "nvue",
+    "虚拟列表"
+],
+  "repository": "https://github.com/SmileZXLee/uni-z-paging",
+  "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/z-paging",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+            "app-vue": "y",
+            "app-nvue": "y",
+            "app-harmony": "u",
+            "app-uvue": "u"
+        },
+        "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"
+        }
+      }
+    }
+  }
+}

+ 57 - 0
uni_modules/z-paging/readme.md

@@ -0,0 +1,57 @@
+# z-paging
+
+<p align="center">
+    <img alt="logo" src="https://z-paging.zxlee.cn/img/title-logo.png" height="100" style="margin-bottom: 50px;" />
+</p>
+
+[![version](https://img.shields.io/badge/version-2.8.7-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)
+<img height="0" width="0" src="https://api.z-notify.zxlee.cn/v1/public/statistics/8293556910106066944/addOnly?from=uni" />
+
+`z-paging-x`现已支持uniapp x,持续完善中,插件地址👉🏻 [https://ext.dcloud.net.cn/plugin?name=z-paging-x](https://ext.dcloud.net.cn/plugin?name=z-paging-x)  
+
+### 文档地址:[https://z-paging.zxlee.cn](https://z-paging.zxlee.cn)
+
+### 更新组件前,请注意[版本差异](https://z-paging.zxlee.cn/start/upgrade-guide.html)
+
+***  
+### 功能&特点
+* 【配置简单】仅需两步(绑定网络请求方法、绑定分页结果数组)轻松完成完整下拉刷新,上拉加载更多功能。
+* 【低耦合,低侵入】分页自动管理。在page中无需处理任何分页相关逻辑,无需在data中定义任何分页相关变量,全由z-paging内部处理。
+* 【超灵活,支持各种类型自定义】支持自定义下拉刷新,自定义上拉加载更多等各种自定义效果;支持使用内置自动分页,同时也支持通过监听下拉刷新和滚动到底部事件自行处理;支持使用自带全屏布局规范,同时也支持将z-paging自由放在任意容器中。
+* 【功能丰富】支持国际化,支持自定义且自动管理空数据图,支持主题模式切换,支持本地分页,支持无闪动聊天分页模式,支持展示最后更新时间,支持吸顶效果,支持内部scroll-view滚动与页面滚动,支持一键滚动到顶部,支持下拉进入二楼等诸多功能。
+* 【【全平台兼容】支持vue&nvue,vue2&vue3,js&ts,支持h5、app、鸿蒙Next及各家小程序。
+* 【高性能】在app-vue、h5、微信小程序、QQ小程序上使用wxs+renderjs在视图层实现下拉刷新;支持虚拟列表,轻松渲染百万级列表数据!
+
+*** 
+### 反馈qq群
+* 官方1群`已满`:[790460711](https://jq.qq.com/?_wv=1027&k=vU2fKZZH)
+
+* 官方2群`已满`:[371624008](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=avPmibADf2TNi4LxkIwjCE5vbfXpa-r1&authKey=dQ%2FVDAR87ONxI4b32Py%2BvmXbhnopjHN7%2FJPtdsqJdsCPFZB6zDQ17L06Uh0kITUZ&noverify=0&group_code=371624008)
+* 官方3群:[343409055](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=sIaNqiCMIjxGQVksjytCw6R8DSiibHR7&authKey=pp995q8ZzFtl5F2xUwvvceP24QTcguWW%2FRVoDnMa8JZF4L2DmS%2B%2FV%2F5sYrcgPsmW&noverify=0&group_code=343409055)
+ 
+*** 
+
+### 预览
+
+***
+
+|                    自定义下拉刷新效果演示                    |                   滑动切换选项卡+吸顶演示                    | 聊天记录模式演示                                             |
+| :----------------------------------------------------------: | :----------------------------------------------------------: | ------------------------------------------------------------ |
+| ![](https://z-paging.zxlee.cn/public/img/z-paging-demo5.gif) | ![](https://z-paging.zxlee.cn/public/img/z-paging-demo6.gif) | ![](https://z-paging.zxlee.cn/public/img/z-paging-demo7.gif) |
+
+|                 虚拟列表(流畅渲染1万+条)演示                 |                       下拉进入二楼演示                       | 在弹窗内使用演示                                             |
+| :----------------------------------------------------------: | :----------------------------------------------------------: | ------------------------------------------------------------ |
+| ![](https://z-paging.zxlee.cn/public/img/z-paging-demo8.gif) | ![](https://z-paging.zxlee.cn/public/img/z-paging-demo9.gif) | ![](https://z-paging.zxlee.cn/public/img/z-paging-demo10.gif) |
+
+
+### 在线demo体验地址:
+
+* [https://demo.z-paging.zxlee.cn](https://demo.z-paging.zxlee.cn)
+
+| 扫码体验                                                     |
+| ------------------------------------------------------------ |
+| ![](https://z-paging.zxlee.cn/public/img/code.png) |
+
+### demo下载
+* 支持vue2&vue3的`选项式api`写法demo下载,请点击页面右上角的【使用HBuilderX导入示例项目】或【下载示例项目ZIP】。
+* 支持vue3的`组合式api`写法demo下载,请访问[github](https://github.com/SmileZXLee/uni-z-paging)。

+ 11 - 0
uni_modules/z-paging/types/comps.d.ts

@@ -0,0 +1,11 @@
+declare module 'vue' {
+  export interface GlobalComponents {
+    ['z-paging']: typeof import('./comps/z-paging')['ZPaging']
+    ['z-paging-swiper']: typeof import('./comps/z-paging-swiper')['ZPagingSwiper']
+    ['z-paging-swiper-item']: typeof import('./comps/z-paging-swiper-item')['ZPagingSwiperItem']
+    ['z-paging-empty-view']: typeof import('./comps/z-paging-empty-view')['ZPagingEmptyView']
+    ['z-paging-cell']: typeof import('./comps/z-paging-cell')['ZPagingCell']
+  }
+}
+
+export {}

+ 9 - 0
uni_modules/z-paging/types/comps/_common.d.ts

@@ -0,0 +1,9 @@
+export interface AllowedComponentProps {
+  class?: unknown;
+  style?: unknown;
+}
+
+export interface VNodeProps {
+  key?: string | number | symbol;
+  ref?: unknown;
+}

+ 29 - 0
uni_modules/z-paging/types/comps/z-paging-cell.d.ts

@@ -0,0 +1,29 @@
+import { AllowedComponentProps, VNodeProps } from './_common'
+
+// ****************************** Props ******************************
+declare interface ZPagingCellProps {
+  /**
+   * z-paging-cell样式
+   */
+  cellStyle?: Partial<CSSStyleDeclaration>
+}
+
+// ****************************** Slots ******************************
+declare interface ZPagingCellSlots {
+  // ******************** 主体布局Slot ********************
+  /**
+   * 默认插入的view
+   */
+  ['default']?: () => any
+}
+
+declare interface _ZPagingCell {
+  new (): {
+    $props: AllowedComponentProps &
+      VNodeProps &
+      ZPagingCellProps
+    $slots: ZPagingCellSlots
+  }
+}
+
+export declare const ZPagingCell: _ZPagingCell

+ 95 - 0
uni_modules/z-paging/types/comps/z-paging-empty-view.d.ts

@@ -0,0 +1,95 @@
+import { AllowedComponentProps, VNodeProps } from './_common'
+
+// ****************************** Props ******************************
+declare interface ZPagingEmptyViewProps {
+  /**
+   * 空数据图片是否铺满z-paging,默认为是。若设置为否,则为填充满z-paging的剩余部分
+   * @default false
+   * @since 2.0.3
+   */
+  emptyViewFixed?: boolean;
+
+  /**
+   * 空数据图描述文字
+   * @default "没有数据哦~"
+   */
+  emptyViewText?: string;
+
+  /**
+   * 空数据图图片,默认使用z-paging内置的图片
+   * - 建议使用绝对路径,开头不要添加"@",请以"/"开头
+   */
+  emptyViewImg?: string;
+
+  /**
+   * 空数据图点击重新加载文字
+   * @default "重新加载"
+   * @since 1.6.7
+   */
+  emptyViewReloadText?: string;
+
+  /**
+   * 空数据图样式,可设置空数据view的top等
+   * - 如果空数据图不是fixed布局,则此处是`margin-top`
+   */
+  emptyViewStyle?: Partial<CSSStyleDeclaration>;
+
+  /**
+   * 空数据图img样式
+   */
+  emptyViewImgStyle?: Partial<CSSStyleDeclaration>;
+
+  /**
+   * 空数据图描述文字样式
+   */
+  emptyViewTitleStyle?: Partial<CSSStyleDeclaration>;
+
+  /**
+   * 空数据图重新加载按钮样式
+   * @since 1.6.7
+   */
+  emptyViewReloadStyle?: Partial<CSSStyleDeclaration>;
+
+  /**
+   * 是否显示空数据图重新加载按钮(无数据时)
+   * @default false
+   * @since 1.6.7
+   */
+  showEmptyViewReload?: boolean;
+
+  /**
+   * 是否是加载失败
+   * @default false
+   */
+  isLoadFailed?: boolean;
+
+  /**
+   * 空数据图中布局的单位
+   * @default 'rpx'
+   * @since 2.6.7
+   */
+  unit?: 'rpx' | 'px';
+
+  // ****************************** Events ******************************
+  /**
+   * 点击了重新加载按钮
+   */
+  onReload?: () => void
+
+  /**
+   * 点击了空数据view
+   * @since 2.3.3
+   */
+  onViewClick?: () => void
+}
+
+declare interface _ZPagingEmptyView {
+  new (): {
+    $props: AllowedComponentProps &
+      VNodeProps &
+      ZPagingEmptyViewProps
+  }
+}
+
+export declare const ZPagingEmptyView: _ZPagingEmptyView
+

+ 95 - 0
uni_modules/z-paging/types/comps/z-paging-swiper-item.d.ts

@@ -0,0 +1,95 @@
+import { AllowedComponentProps, VNodeProps } from './_common'
+
+// ****************************** Props ******************************
+declare interface ZPagingSwiperItemProps {
+  /**
+   * 当前组件的index,也就是当前组件是swiper中的第几个
+   * @default 0
+   */
+  tabIndex?: number
+
+  /**
+   * 当前swiper切换到第几个index
+   * @default 0
+   */
+  currentIndex?: number
+
+  /**
+   * 是否使用虚拟列表。使用页面滚动或nvue时,不支持虚拟列表。在nvue中z-paging内置了list组件,效果与虚拟列表类似,并且可以提供更好的性能。
+   * @default false
+   */
+  useVirtualList?: boolean
+
+  /**
+   * 虚拟列表cell高度模式,默认为`fixed`,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。
+   * @default 'fixed'
+   */
+  cellHeightMode?: 'fixed' | 'dynamic'
+
+  /**
+   * 预加载的列表可视范围(列表高度)页数。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题。
+   * @default 12
+   */
+  preloadPage?: number | string
+
+  /**
+   * 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2。
+   * @default 1
+   * @since 2.2.8
+   */
+  virtualListCol?: number | string
+
+  /**
+   * 虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
+   * @default 80
+   */
+  virtualScrollFps?: number | string
+
+  /**
+   * 是否在z-paging内部循环渲染列表(使用内置列表)。
+   * @default false
+   */
+  useInnerList?: boolean
+
+  /**
+   * 内置列表cell的key名称(仅nvue有效,在nvue中开启use-inner-list时必须填此项)
+   * @since 2.2.7
+   */
+  cellKeyName?: string
+
+  /**
+   * innerList样式
+   */
+  innerListStyle?: Partial<CSSStyleDeclaration>
+}
+
+// ****************************** Methods ******************************
+declare interface _ZPagingSwiperItemRef {
+  /**
+   * 重新加载分页数据,pageNo恢复为默认值,相当于下拉刷新的效果
+   *
+   * @param [animate=false] 是否展示下拉刷新动画
+   */
+  reload: (animate?: boolean) => void;
+
+  /**
+   * 请求结束
+   * - 当通过complete传进去的数组长度小于pageSize时,则判定为没有更多了
+   *
+   * @param [data] 请求结果数组
+   * @param [success=true] 是否请求成功
+   */
+  complete: (data?: any[] | false, success?: boolean) => void;
+}
+
+declare interface _ZPagingSwiperItem {
+  new (): {
+    $props: AllowedComponentProps &
+      VNodeProps &
+      ZPagingSwiperItemProps
+  }
+}
+
+export declare const ZPagingSwiperItem: _ZPagingSwiperItem
+
+export declare const ZPagingSwiperItemRef: _ZPagingSwiperItemRef

+ 89 - 0
uni_modules/z-paging/types/comps/z-paging-swiper.d.ts

@@ -0,0 +1,89 @@
+import { AllowedComponentProps, VNodeProps } from './_common'
+
+// ****************************** Props ******************************
+declare interface ZPagingSwiperProps {
+  /**
+   * 是否使用fixed布局,若使用fixed布局,则z-paging-swiper的父view无需固定高度,z-paging高度默认铺满屏幕,页面中的view请放z-paging-swiper标签内,需要固定在顶部的view使用slot="top"包住,需要固定在底部的view使用slot="bottom"包住。
+   * @default true
+   */
+  fixed?: boolean
+
+  /**
+   * 是否开启底部安全区域适配
+   * @default false
+   */
+  safeAreaInsetBottom?: boolean
+
+  /**
+   * z-paging-swiper样式
+   */
+  swiperStyle?: Partial<CSSStyleDeclaration>
+}
+
+
+// ****************************** Slots ******************************
+declare interface ZPagingSwiperSlots {
+  // ******************** 主体布局Slot ********************
+  /**
+   * 默认插入的view
+   */
+  ['default']?: () => any
+
+  /**
+   * 可以将自定义导航栏、tab-view等需要固定的(不需要跟着滚动的)元素放入slot="top"的view中。
+   * 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="top"。需要固定在顶部的view请勿设置position: fixed;
+   * @since 1.5.5
+   */
+  ['top']?: () => any
+
+  /**
+   * 可以将需要固定在底部的(不需要跟着滚动的)元素放入slot="bottom"的view中。
+   * 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="bottom"。需要固定在底部的view请勿设置position: fixed;
+   * @since 1.6.2
+   */
+  ['bottom']?: () => any
+
+  /**
+   * 可以将需要固定在左侧的(不需要跟着滚动的)元素放入slot="left"的view中。
+   * 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="left"。需要固定在左侧的view请勿设置position: fixed;
+   * @since 2.2.3
+   */
+  ['left']?: () => any
+
+  /**
+   * 可以将需要固定在左侧的(不需要跟着滚动的)元素放入slot="right"的view中。
+   * 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="right"。需要固定在右侧的view请勿设置position: fixed;
+   * @since 2.2.3
+   */
+  ['right']?: () => any
+}
+
+// ****************************** Methods ******************************
+declare interface _ZPagingSwiperRef {
+  /**
+   * 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变后调用
+   *
+   * @since 2.3.5
+   */
+  updateLeftAndRightWidth: () => void;
+
+  /**
+   * 更新fixed模式下z-paging-swiper的布局,在onShow时候调用,以修复在iOS+h5+tabbar+fixed+底部有安全区域的设备中从tabbar页面跳转到无tabbar页面后返回,底部有一段空白区域的问题
+   *
+   * @since 2.6.5
+   */
+  updateFixedLayout: () => void;
+}
+
+declare interface _ZPagingSwiper {
+  new (): {
+    $props: AllowedComponentProps &
+      VNodeProps &
+      ZPagingSwiperProps
+    $slots: ZPagingSwiperSlots
+  }
+}
+
+export declare const ZPagingSwiper: _ZPagingSwiper
+
+export declare const ZPagingSwiperRef: _ZPagingSwiperRef

File diff suppressed because it is too large
+ 2131 - 0
uni_modules/z-paging/types/comps/z-paging.d.ts


+ 24 - 0
uni_modules/z-paging/types/index.d.ts

@@ -0,0 +1,24 @@
+/// <reference path="./comps.d.ts" />
+declare module 'z-paging' {
+  export function install() : void
+  /**
+   * z-paging全局配置
+   * - uni.$zp
+   *
+   * @since 2.6.5
+   */
+  interface $zp {
+    /**
+     * 全局配置
+     */
+    config : Record<string, any>;
+  }
+  global {
+    interface Uni {
+      $zp : $zp
+    }
+  }
+}
+
+declare type ZPagingSwiperRef = typeof import('./comps/z-paging-swiper')['ZPagingSwiperRef']
+declare type ZPagingSwiperItemRef = typeof import('./comps/z-paging-swiper-item')['ZPagingSwiperItemRef']