Browse Source

合并更改

yanghao 2 tuần trước cách đây
mục cha
commit
4647eb1256
26 tập tin đã thay đổi với 1538 bổ sung366 xóa
  1. 17 0
      api/recordFilling.js
  2. 1 0
      locale/zh-Hans.json
  3. 793 0
      pages/recordFilling/detail-old.vue
  4. 182 175
      pages/recordFilling/detail.vue
  5. 1 1
      request/index.js
  6. 19 0
      uni_modules/z-paging/changelog.md
  7. 4 1
      uni_modules/z-paging/components/z-paging/css/z-paging-main.css
  8. 10 6
      uni_modules/z-paging/components/z-paging/js/modules/chat-record-mode.js
  9. 25 15
      uni_modules/z-paging/components/z-paging/js/modules/data-handle.js
  10. 1 1
      uni_modules/z-paging/components/z-paging/js/modules/empty.js
  11. 4 4
      uni_modules/z-paging/components/z-paging/js/modules/load-more.js
  12. 34 3
      uni_modules/z-paging/components/z-paging/js/modules/nvue.js
  13. 9 5
      uni_modules/z-paging/components/z-paging/js/modules/refresher.js
  14. 82 16
      uni_modules/z-paging/components/z-paging/js/modules/scroller.js
  15. 2 18
      uni_modules/z-paging/components/z-paging/js/modules/virtual-list.js
  16. 1 1
      uni_modules/z-paging/components/z-paging/js/z-paging-constant.js
  17. 52 29
      uni_modules/z-paging/components/z-paging/js/z-paging-main.js
  18. 21 1
      uni_modules/z-paging/components/z-paging/js/z-paging-utils.js
  19. 156 20
      uni_modules/z-paging/components/z-paging/z-paging.vue
  20. 41 43
      uni_modules/z-paging/package.json
  21. 2 2
      uni_modules/z-paging/readme.md
  22. 1 1
      uni_modules/z-paging/types/comps/z-paging-cell.d.ts
  23. 4 4
      uni_modules/z-paging/types/comps/z-paging-empty-view.d.ts
  24. 1 1
      uni_modules/z-paging/types/comps/z-paging-swiper-item.d.ts
  25. 1 1
      uni_modules/z-paging/types/comps/z-paging-swiper.d.ts
  26. 74 18
      uni_modules/z-paging/types/comps/z-paging.d.ts

+ 17 - 0
api/recordFilling.js

@@ -80,4 +80,21 @@ export function recordFillingUpOperationOrder(data) {
 		method: 'POST',
 		data: data
 	})
+}
+
+// 获取运行记录填报工单详情-设备列表+属性
+export function recordFillingDetailGetPageAndAttrs(params) {
+	return request({
+		url: '/rq/iot-opeation-fill/orderFillpage1',
+		method: 'GET',
+		params
+	});
+}
+// 保存运行记录填报工单详情填报内容
+export function recordFillingDetailInsertDataList(data) {
+	return request({
+		url: '/rq/iot-opeation-fill/insertDataList',
+		method: 'POST',
+		data: data
+	})
 }

+ 1 - 0
locale/zh-Hans.json

@@ -169,6 +169,7 @@
   "operationRecordFilling.totalDeviceCount": "应填设备数",
   "operationRecordFilling.filledDeviceCount": "已填设备数",
   "operationRecordFilling.unfilledDeviceCount": "未填设备数",
+  "operationRecordFilling.PleaseLoadAllItems": "请加载所有填报项后再提交",
   // --------------------------------------- 状态相关 ----------------------------------------
   "status.enable": "启用",
   "status.disable": "停用",

+ 793 - 0
pages/recordFilling/detail-old.vue

@@ -0,0 +1,793 @@
+<template>
+  <z-paging
+    class="page"
+    ref="paging"
+    v-model="dataList"
+    :loading-more-enabled="false"
+    @query="queryList"
+  >
+    <!-- z-paging默认铺满全屏,此时页面所有view都应放在z-paging标签内,否则会被盖住 -->
+    <!-- 需要固定在页面顶部的view请通过slot="top"插入,包括自定义的导航栏 -->
+    <view class="list">
+      <!-- 工单基础信息 -->
+      <view class="item top">
+        <view class="item-content flex-row align-center">
+          <view class="item-title full-cell flex-row align-center">
+            <span class="item-title-width"
+              >{{ $t("operationRecordFilling.workOrderName") }}:</span
+            >
+            <span>{{ params.orderName }}</span>
+          </view>
+        </view>
+        <view class="item-content flex-row align-center">
+          <view class="item-title full-cell flex-row align-center">
+            <span class="item-title-width"
+              >{{ $t("operationRecordFilling.responsiblePerson") }}:</span
+            >
+            <span>{{ params.userName }}</span>
+          </view>
+        </view>
+        <view class="item-content flex-row align-center">
+          <view class="item-title full-cell flex-row align-center">
+            <span class="item-title-width"
+              >{{ $t("operation.createTime") }}:</span
+            >
+            <span>{{ params.createTime }}</span>
+          </view>
+        </view>
+      </view>
+      <!-- 填报列表 -->
+      <view class="item" v-for="(item, index) in dataList" :key="index">
+        <view class="item-module flex-row align-center justify-between">
+          <view class="module-name">
+            {{ item.deviceCode }}({{ item.deviceName }})
+          </view>
+          <view class="module-border"> </view>
+        </view>
+        <view class="item-content flex-row align-center justify-between bold">
+          <view class="item-title flex-row align-center">
+            <span>{{ $t("operationRecordFilling.belongToTeam") }}:</span>
+            <span>{{ item.orgName }}</span>
+          </view>
+        </view>
+        <view
+          class="item-content flex-row align-center justify-between bold"
+          v-for="sum in item.sumList"
+        >
+          <view class="item-title flex-row align-center word-break-all">
+            <span>{{ sum.name }}:</span>
+          </view>
+          <view class="item-value flex-row align-center justify-end total">
+            <uni-easyinput
+              style="text-align: right"
+              :inputBorder="false"
+              :clearable="true"
+              :styles="{ disableColor: '#fff' }"
+              :value="`${sum.totalRunTime} ${
+                sum.modelAttr ? (sum.modelAttr.includes('Time') ? 'h' : '') : ''
+              }`"
+              :disabled="true"
+            ></uni-easyinput>
+          </view>
+        </view>
+        <view
+          class="item-content flex-col align-center justify-between"
+          :class="{ 'bottom-bold': item.nonSumList.length > 0 }"
+          v-for="nosum in item.nonSumList"
+        >
+          <!-- isCollection为1,提示:以下数值取自PLC,如有不符请修改 -->
+          <uni-notice-bar
+            :text="$t('operationRecordFilling.plcNotice')"
+            v-if="nosum.isCollection == 1"
+          />
+          <view class="flex-row align-center justify-between item-content">
+            <view class="item-title flex-row align-center">
+              <span>{{ nosum.name }}:</span>
+            </view>
+            <!-- 判断填写项的属性 -->
+            <!-- type为double时,输入框为数字类型 -->
+            <view
+              class="item-value flex-row align-center justify-end"
+              v-if="nosum.type == 'double'"
+            >
+              <uni-easyinput
+                style="text-align: right"
+                :styles="{ disableColor: '#fff' }"
+                :inputBorder="false"
+                :clearable="true"
+                :placeholder="$t('operation.PleaseFillIn')"
+                :disabled="!isView"
+                v-model="nosum.fillContent"
+                :type="'digit'"
+                @blur="
+                  nosum.threshold > 0
+                    ? checkThreshold(nosum)
+                    : checkLessThreshold(nosum)
+                "
+                @change="handleFillContentChange(nosum, item)"
+              ></uni-easyinput>
+            </view>
+            <!-- type为textarea时,输入框为文本类型 -->
+            <view
+              class="item-value flex-row align-center justify-end"
+              v-else-if="nosum.type == 'textarea'"
+            >
+              <uni-easyinput
+                style="text-align: right"
+                :styles="{ disableColor: '#fff' }"
+                :inputBorder="false"
+                :clearable="true"
+                :placeholder="$t('operation.PleaseFillIn')"
+                :disabled="!isView"
+                v-model="nosum.fillContent"
+                :type="'textarea'"
+                :autoHeight="true"
+                :maxlength="-1"
+              ></uni-easyinput>
+            </view>
+            <!-- type为enum时,使用下拉菜单 -->
+            <view
+              class="item-value textarea flex-row align-center justify-end"
+              v-else-if="nosum.type == 'enum' && nosum.description !== null"
+            >
+              <uni-data-select
+                :localdata="nosum.enumList"
+                :styles="{ disableColor: '#fff' }"
+                :clear="false"
+                :disabled="!isView"
+                :placeholder="$t('operation.PleaseSelect')"
+                v-model="nosum.fillContent"
+              ></uni-data-select>
+            </view>
+            <!-- 其他类型时,输入框为文本类型 -->
+            <view class="item-value flex-row align-center justify-end" v-else>
+              <uni-easyinput
+                style="text-align: right"
+                :styles="{ disableColor: '#fff' }"
+                :inputBorder="false"
+                :clearable="true"
+                :placeholder="$t('operation.PleaseFillIn')"
+                :disabled="!isView"
+                v-model="nosum.fillContent"
+                :type="'text'"
+              ></uni-easyinput>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+    <!-- 如果需要使用页脚,请使用slot="bottom"slot节点不支持通过v-if或v-show动态显示/隐藏,若需要动态控制,可将v-if添加在其子节点上 -->
+    <template #bottom>
+      <button
+        style="border-radius: 0"
+        type="primary"
+        @click="onSubmit()"
+      >
+        {{ $t("operation.save") }}
+      </button>
+    </template>
+  </z-paging>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance, watch, onMounted } from "vue";
+import { onReady, onLoad } from "@dcloudio/uni-app";
+import dayjs from "dayjs";
+import {
+  getRecordFillingDetailGetPage,
+  getRecordFillingDetailGetAttrs,
+  recordFillingDetailInsertLog,
+  getRecordFillingDetail,
+  recordFillingUpOperationOrder,
+} from "@/api/recordFilling";
+import { getUserId, reloginByUserId } from "@/utils/auth.js";
+import { useDataDictStore } from "@/store/modules/dataDict";
+// 引用全局变量$t
+const { appContext } = getCurrentInstance();
+const t = appContext.config.globalProperties.$t;
+
+// 获取字典项
+const { getStrDictOptions, getIntDictOptions } = useDataDictStore();
+
+// -------------------------------------
+const isFromMsg = ref(false);
+const params = ref({});
+const isView = ref(false); // 是否编辑 -- view == 1为编辑状态
+
+onMounted(() => {
+  console.log("onMounted");
+});
+
+onReady(() => {
+  console.log("onReady");
+});
+
+onLoad(async (option) => {
+  console.log("onLoad", option);
+  await reloginByUserId(option.reloginUserId);
+  isFromMsg.value = !!option.reloginUserId;
+  // 初始化params
+  params.value = JSON.parse(option.param);
+  // 处理createTime
+  params.value.createTime = params.value.createTime
+    ? dayjs(Number.parseInt(params.value.createTime)).format("YYYY-MM-DD")
+    : "";
+  // 请求工单详情
+  if (params.value?.orderId) {
+    const detail = (await getRecordFillingDetail(params.value.orderId)).data;
+    console.log("🚀getRecordFillingDetail ~ detail:", detail);
+    params.value = {
+      ...params.value,
+      ...detail,
+      // 处理createTime
+      createTime: detail.createTime
+        ? dayjs(Number.parseInt(detail.createTime)).format("YYYY-MM-DD")
+        : "",
+      orderId: detail.id,
+    };
+  }
+
+  console.log("🚀 ~ params.value:", params.value);
+  // 处理是否可编辑 {0: '待填写', 1: '已完成', 2: '填写中', 3: '忽略'}
+  isView.value = params.value?.orderStatus % 2 == 0;
+  console.log("🚀 ~ isView.value:", isView.value);
+});
+const paging = ref(null);
+// v-model绑定的这个变量不要在分页请求结束中自己赋值,直接使用即可
+const dataList = ref([]);
+
+// 监听dataList变化,初始化时计算一次总和
+watch(
+  dataList,
+  (newVal) => {
+    // calculateTotalRunTime();
+  },
+  { deep: true }
+);
+
+// 处理fillContent变化的方法
+const handleFillContentChange = (nosum, deviceItem) => {
+  console.log("🚀 ~ nosum, deviceItem:", nosum, deviceItem);
+  // 处理增压机
+  if (
+    deviceItem.deviceName.includes("增压机") &&
+    nosum.name === "当日运转时间"
+  ) {
+    calculateTotalRunTime("增压机", "当日运转时间"); // 计算当日运转时间总和
+  }
+  // 处理提纯撬
+  if (deviceItem.deviceName.includes("提纯撬") && nosum.name === "当日注气量") {
+    calculateTotalRunTime("提纯撬", "当日注气量"); // 计算当日注气量总和
+  }
+  // 处理注水泵
+  if (deviceItem.deviceName.includes("注水泵") && nosum.name === "当日注水量") {
+    calculateTotalRunTime("注水泵", "当日注水量"); // 计算当日注水量总和
+  }
+  // 处理箱式变电站
+  if (
+    deviceItem.deviceName.includes("箱式变电站") &&
+    nosum.name === "当日用电量"
+  ) {
+    calculateTotalRunTime("箱式变电站", "当日用电量"); // 计算当日用电量总和
+  }
+};
+
+/**
+ * 计算所有deviceName中包含deviceNameToMatch的对象中对应的reportName的fillContent总和并更新到reportName的fillContent中
+ * @param deviceNameToMatch {string} 设备名称包含的字符串
+ * @param reportName {string} 填写项名称
+ */
+const calculateTotalRunTime = (deviceNameToMatch, reportName) => {
+  console.log(
+    "🚀calculateTotalRunTime ~ deviceNameToMatch, reportName:",
+    deviceNameToMatch,
+    12
+  );
+  // 查找isReport为1的对象
+  const reportItem = dataList.value.find((item) => item.isReport === 1);
+  console.log("🚀calculateTotalRunTime ~ reportItem:", reportItem);
+
+  if (!reportItem) return;
+  /**
+   * @param deviceNameToMatch {string} 设备名称包含的字符串
+   * @param deviceName {string} 设备名称
+   * @param reportName {string} 填写项名称
+   * 查找[生产日报]中对应的填写项
+   * reportName -> deviceName:reportName
+   * 当日运转时间 -> 增压机:当日运转时间
+   * 当日注气量 -> 提纯撬:当日注气量
+   * 当日注水量 -> 注水泵:当日注水量
+   * 当日用电量 -> 箱式变电站:当日用电量
+   */
+  const targetItem = reportItem.nonSumList.find(
+    (item) => item.name === reportName
+  );
+
+  if (!targetItem) return;
+
+  // 计算所有deviceName中包含deviceNameToMatch的对象中对应的reportName的fillContent总和
+  let total = null;
+  dataList.value.forEach((item) => {
+    if (item.deviceName.includes(deviceNameToMatch) && item.nonSumList) {
+      item.nonSumList.forEach((nonSum) => {
+        // 只累加数字类型的值
+        if (
+          nonSum.type === "double" &&
+          nonSum.fillContent &&
+          nonSum.name === reportName
+        ) {
+          console.log("🚀 ~ nonSum.fillContent:", nonSum.fillContent);
+          console.log("🚀 ~ nonSum:", nonSum);
+          const value = Number(nonSum.fillContent) || 0;
+          total += value;
+        }
+      });
+    }
+  });
+  console.log("🚀 ~ total:", total);
+  if (total !== null) {
+    // 更新目标值,保留两位小数
+    targetItem.fillContent = toFixed(total);
+    console.log("🚀 ~ targetItem.fillContent:", targetItem.fillContent);
+  }
+};
+
+// @query所绑定的方法不要自己调用!!需要刷新列表数据时,只需要调用paging.value.reload()即可
+const queryList = (pageNo, pageSize) => {
+  const userId = uni.getStorageSync("userId");
+  if (!userId) {
+    paging.value.complete([]);
+    return;
+  }
+
+  // 此处请求仅为演示,请替换为自己项目中的请求
+  getRecordFillingDetailGetPage({
+    // pageNo,
+    // pageSize,
+    ...params.value,
+    deviceCategoryId: 1,
+  })
+    .then(async (res) => {
+      const resList = [].concat(res.data);
+
+      // 使用Promise.all等待所有异步请求完成
+      await Promise.all(
+        resList.map(async (item) => {
+          try {
+            const attrParams = {
+              deviceCode: item.deviceCode,
+              deviceName: item.deviceName,
+              deptId: item.deptId,
+              createTime: params.value.createTime,
+              deviceCategoryId: item.deviceCategoryId,
+              deviceId: item.deviceId,
+              userId: params.value.userId,
+              orderId: params.value.orderId,
+            };
+            // console.log(
+            //   "getRecordFillingDetailGetAttrs- attrParams",
+            //   attrParams
+            // );
+            const resAttrs = await getRecordFillingDetailGetAttrs({
+              pageNo: 1,
+              pageSize: 10,
+              ...attrParams,
+            });
+            // console.log("resAttrs", resAttrs);
+            if (resAttrs?.data) {
+              attrParams.createTime = attrParams.createTime
+                ? dayjs(attrParams.createTime).format("YYYY-MM-DD")
+                : "";
+              attrParams.id = attrParams.orderId;
+              delete attrParams.orderId;
+              delete attrParams.deviceName;
+              resAttrs.data.map((rtem) => {
+                // 将rtem中sumList和nonSumList两个数组中的
+                // fillContent字段判断是否为null, 如果为null,则赋值为0 不为null则保留两位小数
+                // 将attrParams合并到两个数组的每个对象中
+                // 然后将sumList和nonSumList分别赋值给item的sumList和nonSumList
+
+                if (rtem.sumList) {
+                  rtem.sumList.map((sumItem) => {
+                    if (
+                      sumItem.fillContent == null ||
+                      sumItem.fillContent == ""
+                    ) {
+                      // console.log("🚀 ~ rtem.sumList.map ~ sumItem:", sumItem);
+                      // sumItem.fillContent = 0;
+                    } else {
+                      // 如果是double类型,保留两位小数
+                      if (sumItem.type == "double") {
+                        sumItem.fillContent = toFixed(sumItem.fillContent);
+                      }
+                    }
+                    // 将sumItem的id赋值给modelId
+                    sumItem.modelId = sumItem.id;
+
+                    sumItem.pointName = sumItem.name;
+                    // 合并attrParams到sumItem中
+                    sumItem = Object.assign(sumItem, attrParams);
+                  });
+                }
+                if (rtem.nonSumList) {
+                  //
+                  rtem.nonSumList.map((nonSumItem) => {
+                    if (
+                      nonSumItem.fillContent == null ||
+                      nonSumItem.fillContent == ""
+                    ) {
+                      // console.log(
+                      //   "🚀 ~ rtem.nonSumList.map ~ nonSumItem:",
+                      //   nonSumItem
+                      // );
+                      // nonSumItem.fillContent = 0;
+                    } else {
+                      // 如果是double类型,保留两位小数
+                      if (nonSumItem.type == "double") {
+                        nonSumItem.fillContent = toFixed(
+                          nonSumItem.fillContent
+                        );
+                      }
+                    }
+                    nonSumItem.pointName = nonSumItem.name;
+                    // 将nonSumItem的id赋值给modelId
+                    nonSumItem.modelId = nonSumItem.id;
+                    // 合并attrParams到nonSumItem中
+                    nonSumItem = Object.assign(nonSumItem, attrParams);
+                    // 如果是enum类型,且description不为null,则根据description获取对应字典项数组,赋值给enumList
+                    if (nonSumItem.type == "enum" && nonSumItem.description) {
+                      console.log("🚀 ~ onSumItem.description:");
+                      nonSumItem.enumList =
+                        nonSumItem.name === "非生产原因"
+                          ? getIntDictOptions(nonSumItem.description).map(
+                              (dict) => {
+                                return {
+                                  ...dict,
+                                  text: dict.label,
+                                };
+                              }
+                            )
+                          : getStrDictOptions(nonSumItem.description).map(
+                              (dict) => {
+                                return {
+                                  ...dict,
+                                  text: dict.label,
+                                };
+                              }
+                            );
+                      console.log(
+                        "🚀 ~  nonSumItem.enumList:",
+                        nonSumItem.enumList
+                      );
+                    }
+                  });
+                }
+                item.sumList = rtem.sumList;
+
+                item.nonSumList = rtem.nonSumList;
+              });
+              console.log("resAttrs-modelId", resAttrs);
+            }
+          } catch (error) {
+            console.error("获取属性失败", error);
+            // 可以选择设置默认值或标记错误状态
+            item.sumList = [];
+            item.nonSumList = [];
+          }
+        })
+      );
+      console.log("resList--", resList);
+      // 将请求结果通过complete传给z-paging处理,同时也代表请求结束,这一行必须调用
+      paging.value.complete(resList);
+    })
+    .catch((res) => {
+      // 如果请求失败写paging.value.complete(false);
+      // 注意,每次都需要在catch中写这句话很麻烦,z-paging提供了方案可以全局统一处理
+      // 在底层的网络请求抛出异常时,写uni.$emit('z-paging-error-emit');即可
+      paging.value.complete(false);
+    });
+};
+// 判断是否小于阈值 (<0)
+const checkLessThreshold = (item) => {
+  if (item.fillContent < 0) {
+    uni.showToast({
+      title:
+        item.name +
+        t("operationRecordFilling.fillContentCannotLessThanThreshold") +
+        "0",
+      icon: "none",
+    });
+    item.fillContent = ""; // 清空输入
+    return false; // 返回false表示校验失败
+  }
+};
+// 判断是否大于阈值
+const checkThreshold = (item) => {
+  checkLessThreshold(item);
+  // 如果threshold > 0,则判断fillContent是否大于threshold,如果大于则提示用户填写小于等于threshold的值
+  if (item.fillContent > item.threshold) {
+    uni.showToast({
+      title:
+        item.name +
+        t("operationRecordFilling.fillContentCannotGreaterThanThreshold") +
+        item.threshold,
+      icon: "none",
+    });
+    item.fillContent = ""; // 清空输入
+    return false; // 返回false表示校验失败
+  }
+};
+
+// 保留两位小数
+const toFixed = (num) => {
+  if (num) {
+    num = Number(num);
+    num = num.toFixed(2);
+  } else {
+    num = 0.0;
+  }
+  return num;
+};
+
+const onSubmit = async () => {
+  console.log("onSubmit", dataList.value);
+  // 1. 校验所有必填项
+  // 遍历dataList.value中nonSumList每个item(非生产日报 isReport!=1)的fillContent字段,
+  // 如果为null或者为空,则提示用户填写,
+  // 如果threshold > 0,则判断fillContent是否大于threshold,如果大于则提示用户填写小于等于threshold的值
+  // 如果所有项全部填写,则调用填写记录接口
+
+  for (const item of dataList.value) {
+    const nonSumList = item.nonSumList;
+    for (const nonSumItem of nonSumList) {
+      if (
+        (!item.isReport || item.isReport != 1) &&
+        (nonSumItem.fillContent == null || nonSumItem.fillContent === "")
+      ) {
+        uni.showToast({
+          title:
+            t("operation.PleaseFillIn") +
+            item.deviceCode +
+            "(" +
+            item.deviceName +
+            ")" +
+            t("operation.allItem"),
+          icon: "none",
+        });
+        return; // 校验失败直接返回
+      }
+      if (nonSumItem.fillContent != "" && nonSumItem.fillContent != null) {
+        console.log("🚀 ~ nonSumItem:", nonSumItem);
+        console.log("🚀 ~ nonSumItem.fillContent:", nonSumItem.fillContent);
+        // 先将值转换为字符串进行操作
+        const fillContentStr = String(nonSumItem.fillContent);
+        //  将字符串转换为数字
+        const num = Number(fillContentStr);
+
+        // 检查转换后的数字是否有效
+        if (!isNaN(num)) {
+          // 检查是否包含小数(使用字符串检查)
+          if (fillContentStr.includes(".")) {
+            // 保留两位小数(假设toFixed是你定义的保留两位小数的函数)
+            nonSumItem.fillContent = toFixed(num);
+          } else {
+            // 转换为整数
+            nonSumItem.fillContent = Math.floor(num);
+          }
+        }
+      }
+      // 如果threshold > 0,则判断fillContent是否大于threshold
+      if (nonSumItem.threshold > 0) {
+        if (nonSumItem.fillContent > nonSumItem.threshold) {
+          uni.showToast({
+            title:
+              item.deviceCode +
+              "(" +
+              item.deviceName +
+              ")" +
+              nonSumItem.name +
+              t(
+                "operationRecordFilling.fillContentCannotGreaterThanThreshold"
+              ) +
+              nonSumItem.threshold,
+            icon: "none",
+            duration: 3000,
+          });
+          nonSumItem.fillContent = ""; // 清空输入
+          return; // 校验失败直接返回
+        }
+      }
+    }
+  }
+  // 定义新的dataList副本 用于提交数据,避免修改原数据
+  const subDataList = JSON.parse(JSON.stringify(dataList.value));
+
+  // 3. 处理副本:删除 enumList(仅修改副本,不影响原数据)
+  for (const item of subDataList) {
+    // 先判断 item.nonSumList 存在,避免空指针
+    if (item.nonSumList && item.nonSumList.length) {
+      for (const nonSumItem of item.nonSumList) {
+        if (nonSumItem.enumList) {
+          delete nonSumItem.enumList;
+        }
+      }
+    }
+  }
+  console.log("提交用的副本数据:subDataList", subDataList);
+
+  try {
+    // 2. 收集所有保存请求(不在这里处理导航)
+    const submitPromises = subDataList.map(async (item) => {
+      const submitData = [].concat(item.sumList).concat(item.nonSumList);
+      // 仅返回接口结果,不执行导航
+      return await recordFillingDetailInsertLog(submitData);
+    });
+
+    // 3. 等待所有请求完成
+    const results = await Promise.all(submitPromises);
+
+    // 4. 所有请求完成后,统一判断结果
+    const allSuccess = results.every((res) => res?.code === 0);
+    if (allSuccess) {
+      // 5. 调用更新工单状态接口
+      const upRes = await recordFillingUpOperationOrder({
+        id: params.value.orderId,
+      });
+      console.log("🚀 ~ upRes:", upRes)
+      if (upRes?.code === 0) {
+        console.log("工单状态更新成功");
+        uni.showToast({
+          title: t("operation.success"),
+          duration: 1500,
+          icon: "none",
+        });
+        setTimeout(() => {
+          uni.navigateBack();
+        }, 1500);
+      } else {
+        console.error("工单状态更新失败", upRes);
+        uni.showToast({
+          title: t("operation.fail"),
+          icon: "none",
+        });
+      }
+    } else {
+      uni.showToast({
+        title: t("operation.fail"),
+        icon: "none",
+      });
+    }
+  } catch (error) {
+    console.error("保存失败", error);
+    uni.showToast({
+      title: t("operation.fail"),
+      icon: "error",
+    });
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page {
+  padding: 0;
+  box-sizing: border-box;
+}
+
+.top {
+  padding: 10px;
+}
+
+.list {
+  // margin-top: calc(10px);
+  padding: 10px;
+  height: calc(100%);
+}
+
+.item {
+  width: 100%;
+  // height: 204px;
+  background: #ffffff;
+  border-radius: 6px;
+  margin-bottom: 10px;
+  box-sizing: border-box;
+  padding: 20px 15px;
+}
+
+.item-module {
+  width: 100%;
+  height: 16px;
+  position: relative;
+  font-weight: 600;
+  font-size: 14px;
+  color: #333333;
+  margin-bottom: 10px;
+
+  .module-border {
+    position: absolute;
+    left: -15px;
+    width: 0px;
+    height: 12px;
+    border: 1px solid #004098;
+  }
+}
+
+.item-content {
+  position: relative;
+  width: 100%;
+  // height: calc(38px);
+  box-sizing: border-box;
+  font-weight: 500;
+  font-size: 14px;
+  color: #333333;
+  line-height: 20px;
+  border-bottom: 1px dashed #cacccf;
+
+  &:last-child {
+    border-bottom: none;
+  }
+
+  &.bold {
+    font-weight: 600;
+    // :deep(.uni-easyinput__content-input){
+    // 	padding-right: 0 !important;
+    // }
+  }
+  &.bottom-bold {
+    border-bottom: 1px dashed #cacccf;
+  }
+}
+
+.item-title {
+  position: relative;
+  min-height: 38px;
+  width: 55%;
+
+  &.total {
+    :deep(.is-disabled) {
+      color: #333333 !important;
+    }
+  }
+  &.full-cell {
+    width: 100%;
+    min-width: max-content;
+  }
+}
+
+.item-value {
+  width: 45%;
+  position: relative;
+  &.textarea {
+    width: 65%;
+  }
+}
+.word-break-all {
+  min-width: unset;
+}
+:deep(.uni-select) {
+  border: none;
+  text-align: right;
+  padding-right: 0;
+  .uniui-bottom:before {
+    content: "\e6b5" !important;
+    font-size: 16px !important;
+  }
+}
+:deep(.uni-select--disabled) {
+  color: #d5d5d5 !important;
+  background-color: transparent;
+  .uni-select__input-text {
+    color: #d5d5d5 !important;
+  }
+}
+:deep(.uni-select__selector) {
+  text-align: left;
+}
+:deep(.uni-select__selector-item) {
+  border-bottom: 1px dashed #cacccf;
+  text-align: left;
+}
+:deep(.uni-easyinput__content-textarea) {
+  min-height: inherit;
+  margin: 10px;
+}
+</style>

+ 182 - 175
pages/recordFilling/detail.vue

@@ -3,12 +3,12 @@
     class="page"
     ref="paging"
     v-model="dataList"
-    :loading-more-enabled="false"
+    :loading-more-enabled="true"
     @query="queryList"
   >
     <!-- z-paging默认铺满全屏,此时页面所有view都应放在z-paging标签内,否则会被盖住 -->
     <!-- 需要固定在页面顶部的view请通过slot="top"插入,包括自定义的导航栏 -->
-    <view class="list">
+    <template #top>
       <!-- 工单基础信息 -->
       <view class="item top">
         <view class="item-content flex-row align-center">
@@ -36,7 +36,9 @@
           </view>
         </view>
       </view>
-      <!-- 填报列表 -->
+    </template>
+    <!-- 填报列表 -->
+    <view class="list">
       <view class="item" v-for="(item, index) in dataList" :key="index">
         <view class="item-module flex-row align-center justify-between">
           <view class="module-name">
@@ -127,11 +129,12 @@
             </view>
             <!-- type为enum时,使用下拉菜单 -->
             <view
-              class="item-value textarea flex-row align-center justify-end"
+              class="item-value select flex-row align-center justify-end"
               v-else-if="nosum.type == 'enum' && nosum.description !== null"
             >
               <uni-data-select
                 :localdata="nosum.enumList"
+                style="text-align: right"
                 :styles="{ disableColor: '#fff' }"
                 :clear="false"
                 :disabled="!isView"
@@ -162,6 +165,8 @@
         style="border-radius: 0"
         type="primary"
         @click="onSubmit()"
+        :disabled="dataList.length < totalNum"
+        v-if="isView"
       >
         {{ $t("operation.save") }}
       </button>
@@ -179,6 +184,8 @@ import {
   recordFillingDetailInsertLog,
   getRecordFillingDetail,
   recordFillingUpOperationOrder,
+  recordFillingDetailGetPageAndAttrs,
+  recordFillingDetailInsertDataList,
 } from "@/api/recordFilling";
 import { getUserId, reloginByUserId } from "@/utils/auth.js";
 import { useDataDictStore } from "@/store/modules/dataDict";
@@ -235,6 +242,8 @@ onLoad(async (option) => {
 const paging = ref(null);
 // v-model绑定的这个变量不要在分页请求结束中自己赋值,直接使用即可
 const dataList = ref([]);
+// 列表总数
+const totalNum = ref(0);
 
 // 监听dataList变化,初始化时计算一次总和
 watch(
@@ -340,142 +349,129 @@ const queryList = (pageNo, pageSize) => {
     return;
   }
 
-  // 此处请求仅为演示,请替换为自己项目中的请求
-  getRecordFillingDetailGetPage({
-    // pageNo,
-    // pageSize,
-    ...params.value,
-    deviceCategoryId: 1,
+  // 请求填报设备及属性
+  recordFillingDetailGetPageAndAttrs({
+    pageNo,
+    pageSize,
+    orderId: params.value.orderId,
+    // deviceCategoryId: 1,
   })
     .then(async (res) => {
-      const resList = [].concat(res.data);
-
-      // 使用Promise.all等待所有异步请求完成
-      await Promise.all(
-        resList.map(async (item) => {
-          try {
-            const attrParams = {
-              deviceCode: item.deviceCode,
-              deviceName: item.deviceName,
-              deptId: item.deptId,
-              createTime: params.value.createTime,
-              deviceCategoryId: item.deviceCategoryId,
-              deviceId: item.deviceId,
-              userId: params.value.userId,
-              orderId: params.value.orderId,
-            };
-            // console.log(
-            //   "getRecordFillingDetailGetAttrs- attrParams",
-            //   attrParams
-            // );
-            const resAttrs = await getRecordFillingDetailGetAttrs({
-              pageNo: 1,
-              pageSize: 10,
-              ...attrParams,
-            });
-            // console.log("resAttrs", resAttrs);
-            if (resAttrs?.data) {
-              attrParams.createTime = attrParams.createTime
-                ? dayjs(attrParams.createTime).format("YYYY-MM-DD")
-                : "";
-              attrParams.id = attrParams.orderId;
-              delete attrParams.orderId;
-              delete attrParams.deviceName;
-              resAttrs.data.map((rtem) => {
-                // 将rtem中sumList和nonSumList两个数组中的
-                // fillContent字段判断是否为null, 如果为null,则赋值为0 不为null则保留两位小数
-                // 将attrParams合并到两个数组的每个对象中
-                // 然后将sumList和nonSumList分别赋值给item的sumList和nonSumList
-
-                if (rtem.sumList) {
-                  rtem.sumList.map((sumItem) => {
-                    if (
-                      sumItem.fillContent == null ||
-                      sumItem.fillContent == ""
-                    ) {
-                      // console.log("🚀 ~ rtem.sumList.map ~ sumItem:", sumItem);
-                      // sumItem.fillContent = 0;
-                    } else {
-                      // 如果是double类型,保留两位小数
-                      if (sumItem.type == "double") {
-                        sumItem.fillContent = toFixed(sumItem.fillContent);
-                      }
-                    }
-                    // 将sumItem的id赋值给modelId
-                    sumItem.modelId = sumItem.id;
-
-                    sumItem.pointName = sumItem.name;
-                    // 合并attrParams到sumItem中
-                    sumItem = Object.assign(sumItem, attrParams);
-                  });
+      console.log("🚀 ~ res:", res);
+      const { data } = res;
+      const resList = [].concat(data.list);
+      // 列表总数
+      totalNum.value = data.total;
+      // 遍历列表,处理attrsDetail
+      resList.map(async (item) => {
+        const attrParams = {
+          deviceCode: item.deviceCode,
+          deviceName: item.deviceName,
+          deptId: item.deptId,
+          createTime: params.value.createTime,
+          deviceCategoryId: item.deviceCategoryId,
+          deviceId: item.deviceId,
+          userId: params.value.userId,
+          orderId: params.value.orderId,
+        };
+        // console.log(
+        //   "getRecordFillingDetailGetAttrs- attrParams",
+        //   attrParams
+        // );
+        const resAttrs = item?.attrsDetail;
+        // console.log("resAttrs", resAttrs);
+        if (resAttrs) {
+          attrParams.createTime = attrParams.createTime
+            ? dayjs(attrParams.createTime).format("YYYY-MM-DD")
+            : "";
+          attrParams.id = attrParams.orderId;
+          delete attrParams.orderId;
+          delete attrParams.deviceName;
+          resAttrs.map((rtem) => {
+            // 将rtem中sumList和nonSumList两个数组中的
+            // fillContent字段判断是否为null, 如果为null,则赋值为0 不为null则保留两位小数
+            // 将attrParams合并到两个数组的每个对象中
+            // 然后将sumList和nonSumList分别赋值给item的sumList和nonSumList
+
+            if (rtem.sumList) {
+              rtem.sumList.map((sumItem) => {
+                if (sumItem.fillContent == null || sumItem.fillContent == "") {
+                  // console.log("🚀 ~ rtem.sumList.map ~ sumItem:", sumItem);
+                  // sumItem.fillContent = 0;
+                } else {
+                  // 如果是double类型,保留两位小数
+                  if (sumItem.type == "double") {
+                    sumItem.fillContent = toFixed(sumItem.fillContent);
+                  }
+                }
+                // 将sumItem的id赋值给modelId
+                sumItem.modelId = sumItem.id;
+
+                sumItem.pointName = sumItem.name;
+                // 合并attrParams到sumItem中
+                sumItem = Object.assign(sumItem, attrParams);
+              });
+            }
+            if (rtem.nonSumList) {
+              //
+              rtem.nonSumList.map((nonSumItem) => {
+                if (
+                  nonSumItem.fillContent == null ||
+                  nonSumItem.fillContent == ""
+                ) {
+                  // console.log(
+                  //   "🚀 ~ rtem.nonSumList.map ~ nonSumItem:",
+                  //   nonSumItem
+                  // );
+                  // nonSumItem.fillContent = 0;
+                } else {
+                  // 如果是double类型,保留两位小数
+                  if (nonSumItem.type == "double") {
+                    nonSumItem.fillContent = toFixed(nonSumItem.fillContent);
+                  }
                 }
-                if (rtem.nonSumList) {
-                  //
-                  rtem.nonSumList.map((nonSumItem) => {
-                    if (
-                      nonSumItem.fillContent == null ||
-                      nonSumItem.fillContent == ""
-                    ) {
-                      // console.log(
-                      //   "🚀 ~ rtem.nonSumList.map ~ nonSumItem:",
-                      //   nonSumItem
-                      // );
-                      // nonSumItem.fillContent = 0;
-                    } else {
-                      // 如果是double类型,保留两位小数
-                      if (nonSumItem.type == "double") {
-                        nonSumItem.fillContent = toFixed(
-                          nonSumItem.fillContent
+                nonSumItem.pointName = nonSumItem.name;
+                // 将nonSumItem的id赋值给modelId
+                nonSumItem.modelId = nonSumItem.id;
+                // 合并attrParams到nonSumItem中
+                nonSumItem = Object.assign(nonSumItem, attrParams);
+                // 如果是enum类型,且description不为null,则根据description获取对应字典项数组,赋值给enumList
+                if (nonSumItem.type == "enum" && nonSumItem.description) {
+                  console.log("🚀 ~ onSumItem.description:");
+                  nonSumItem.enumList =
+                    nonSumItem.name === "非生产原因"
+                      ? getIntDictOptions(nonSumItem.description).map(
+                          (dict) => {
+                            return {
+                              ...dict,
+                              text: dict.label,
+                            };
+                          }
+                        )
+                      : getStrDictOptions(nonSumItem.description).map(
+                          (dict) => {
+                            return {
+                              ...dict,
+                              text: dict.label,
+                            };
+                          }
                         );
-                      }
-                    }
-                    nonSumItem.pointName = nonSumItem.name;
-                    // 将nonSumItem的id赋值给modelId
-                    nonSumItem.modelId = nonSumItem.id;
-                    // 合并attrParams到nonSumItem中
-                    nonSumItem = Object.assign(nonSumItem, attrParams);
-                    // 如果是enum类型,且description不为null,则根据description获取对应字典项数组,赋值给enumList
-                    if (nonSumItem.type == "enum" && nonSumItem.description) {
-                      console.log("🚀 ~ onSumItem.description:");
-                      nonSumItem.enumList =
-                        nonSumItem.name === "非生产原因"
-                          ? getIntDictOptions(nonSumItem.description).map(
-                              (dict) => {
-                                return {
-                                  ...dict,
-                                  text: dict.label,
-                                };
-                              }
-                            )
-                          : getStrDictOptions(nonSumItem.description).map(
-                              (dict) => {
-                                return {
-                                  ...dict,
-                                  text: dict.label,
-                                };
-                              }
-                            );
-                      console.log(
-                        "🚀 ~  nonSumItem.enumList:",
-                        nonSumItem.enumList
-                      );
-                    }
-                  });
+                  console.log(
+                    "🚀 ~  nonSumItem.enumList:",
+                    nonSumItem.enumList
+                  );
                 }
-                item.sumList = rtem.sumList;
-
-                item.nonSumList = rtem.nonSumList;
               });
-              console.log("resAttrs-modelId", resAttrs);
             }
-          } catch (error) {
-            console.error("获取属性失败", error);
-            // 可以选择设置默认值或标记错误状态
-            item.sumList = [];
-            item.nonSumList = [];
-          }
-        })
-      );
+            item.sumList = rtem.sumList;
+
+            item.nonSumList = rtem.nonSumList;
+          });
+          console.log("resAttrs-modelId", resAttrs);
+        }
+        return item;
+      });
+
       console.log("resList--", resList);
       // 将请求结果通过complete传给z-paging处理,同时也代表请求结束,这一行必须调用
       paging.value.complete(resList);
@@ -530,7 +526,15 @@ const toFixed = (num) => {
 };
 
 const onSubmit = async () => {
-  console.log("onSubmit", dataList.value);
+  // console.log("onSubmit", dataList.value);
+  // 校验是否所有待填写项都已加载
+  if (dataList.value.length < totalNum) {
+    uni.showToast({
+      title: t("operationRecordFilling.PleaseLoadAllItems"),
+      icon: "none",
+    });
+    return; // 校验失败直接返回
+  }
   // 1. 校验所有必填项
   // 遍历dataList.value中nonSumList每个item(非生产日报 isReport!=1)的fillContent字段,
   // 如果为null或者为空,则提示用户填写,
@@ -613,57 +617,54 @@ const onSubmit = async () => {
       }
     }
   }
-  console.log("提交用的副本数据:subDataList", subDataList);
-
-  try {
-    // 2. 收集所有保存请求(不在这里处理导航)
-    const submitPromises = subDataList.map(async (item) => {
-      const submitData = [].concat(item.sumList).concat(item.nonSumList);
-      // 仅返回接口结果,不执行导航
-      return await recordFillingDetailInsertLog(submitData);
-    });
-
-    // 3. 等待所有请求完成
-    const results = await Promise.all(submitPromises);
-
-    // 4. 所有请求完成后,统一判断结果
-    const allSuccess = results.every((res) => res?.code === 0);
-    if (allSuccess) {
-      // 5. 调用更新工单状态接口
-      const upRes = await recordFillingUpOperationOrder({
-        id: params.value.orderId,
-      });
-      console.log("🚀 ~ upRes:", upRes)
-      if (upRes?.code === 0) {
-        console.log("工单状态更新成功");
-        uni.showToast({
-          title: t("operation.success"),
-          duration: 1500,
-          icon: "none",
+  console.log("处理提交用的副本数据:subDataList", subDataList);
+  // 2. 处理提交数据:将nonSumList和sumList合并为新数组并赋值给deviceInfoList对象,将所有的deviceInfoList合并为submitList
+  const submitList = subDataList.map((item) => ({
+    deviceInfoList: [].concat(item.sumList).concat(item.nonSumList),
+  }));
+  console.log("提交用的数据:submitList", submitList);
+  // 3. 提交所有填写记录
+
+  await recordFillingDetailInsertDataList(submitList)
+    .then(async (res) => {
+      console.log("🚀 ~ 提交工单填报内容结果 ~ res:", res);
+      if (res?.code === 0) {
+        // 3. 调用更新工单状态接口
+        const upRes = await recordFillingUpOperationOrder({
+          id: params.value.orderId,
         });
-        setTimeout(() => {
-          uni.navigateBack();
-        }, 1500);
+        console.log("🚀 ~ upRes:", upRes);
+        if (upRes?.code === 0) {
+          console.log("工单状态更新成功");
+          uni.showToast({
+            title: t("operation.success"),
+            duration: 1500,
+            icon: "none",
+          });
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 1500);
+        } else {
+          console.error("工单状态更新失败", upRes);
+          uni.showToast({
+            title: t("operation.fail"),
+            icon: "none",
+          });
+        }
       } else {
-        console.error("工单状态更新失败", upRes);
         uni.showToast({
           title: t("operation.fail"),
           icon: "none",
         });
       }
-    } else {
+    })
+    .catch((error) => {
+      console.error("保存失败", error);
       uni.showToast({
         title: t("operation.fail"),
-        icon: "none",
+        icon: "error",
       });
-    }
-  } catch (error) {
-    console.error("保存失败", error);
-    uni.showToast({
-      title: t("operation.fail"),
-      icon: "error",
     });
-  }
 };
 </script>
 
@@ -680,7 +681,7 @@ const onSubmit = async () => {
 .list {
   // margin-top: calc(10px);
   padding: 10px;
-  height: calc(100%);
+  // height: calc(100%);
 }
 
 .item {
@@ -772,6 +773,12 @@ const onSubmit = async () => {
     font-size: 16px !important;
   }
 }
+:deep(.uni-select__input-text) {
+  text-align: right;
+  .align-left {
+    text-align: right;
+  }
+}
 :deep(.uni-select--disabled) {
   color: #d5d5d5 !important;
   background-color: transparent;

+ 1 - 1
request/index.js

@@ -47,7 +47,7 @@ function closeLoading() {
  */
 const http = new Request({
   baseURL: baseUrl + apiPath,
-  timeout: 8000,
+  timeout: 60000,
   method: 'GET',
   header: {
     Accept: 'text/json',

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

@@ -1,3 +1,22 @@
+## 2.8.8(2025-08-29)
+1.`新增` props:`in-swiper-slot`,用以解决在vue3+(微信小程序或QQ小程序)中,`scrollIntoViewById`和`scrollIntoViewByIndex`因无法获取节点信息导致滚动到指定view无效的问题。  
+2.`修复` 在vue2中缓存模式无效的问题。  
+3.`修复` 聊天记录模式在键盘弹出后,底部聊天输入框依然可以滚动的问题。  
+4.`修复` 部分老版本`webview`中,`#right`位置不正确的问题。  
+5.`修复` 在快手小程序+安卓中滚动到底部可能多次触发的问题。  
+6.`修复` 在微信小程序+虚拟列表中滚动到顶部偶现无效的问题。  
+7.`修复` 方法`reload(true)`调用时`refresher-complete-delay`无效的问题。  
+8.`优化` 底部安全区域展示逻辑和性能。   
+## 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`代码文档提示。  

+ 4 - 1
uni_modules/z-paging/components/z-paging/css/z-paging-main.css

@@ -52,6 +52,10 @@
 	/* #endif */
 }
 
+.zp-right {
+	right: 0;
+}
+
 .zp-scroll-view-super {
 	flex: 1;
 	overflow: hidden;
@@ -218,7 +222,6 @@
 }
 
 .zp-safe-area-inset-bottom {
-	position: absolute;
 	/* #ifndef APP-PLUS */
 	height: env(safe-area-inset-bottom);
 	/* #endif */

+ 10 - 6
uni_modules/z-paging/components/z-paging/js/modules/chat-record-mode.js

@@ -111,12 +111,7 @@ export default {
 		}
 	},
 	mounted() {
-		// 监听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
-		// #ifndef H5 || MP-BAIDU || MP-TOUTIAO
-		if (this.useChatRecordMode) {
-			uni.onKeyboardHeightChange(this._handleKeyboardHeightChange);
-		}
-		// #endif
+		this.addKeyboardHeightChangeListener();
 	},
 	methods: {
 		// 添加聊天记录
@@ -129,6 +124,15 @@ export default {
 		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);

+ 25 - 15
uni_modules/z-paging/components/z-paging/js/modules/data-handle.js

@@ -157,7 +157,8 @@ export default {
 			listRendering: false,
 			isHandlingRefreshToPage: false,
 			isFirstPageAndNoMore: false,
-			totalDataChangeThrow: true
+			totalDataChangeThrow: true,
+			addDataFromTopBufferedInsert: u.useBufferedInsert(this._addDataFromTop)
 		}
 	},
 	computed: {
@@ -182,7 +183,9 @@ export default {
 	},
 	watch: {
 		totalData(newVal, oldVal) {
-			this._totalDataChange(newVal, oldVal, this.totalDataChangeThrow);
+			// 触发totalData改变事件时是否触发emit列表更新事件,如果是从缓存中设置则必须触发,否则根据totalDataChangeThrow的规则判断
+			const eventThrow = this.isSettingCacheList ? true : this.totalDataChangeThrow;
+			this._totalDataChange(newVal, oldVal, eventThrow);
 			this.totalDataChangeThrow = true;
 		},
 		currentData(newVal, oldVal) {
@@ -299,17 +302,10 @@ export default {
 		},
 		// 从顶部添加数据,不会影响分页的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));
-			}
+			// 如果使用了虚拟列表,则需要对短时间内的大量数据进行整合然后一次性添加,避免设置虚拟列表cellIndex时候key冲突的问题,否则正常调用
+			(this.finalUseVirtualList ? this.addDataFromTopBufferedInsert : this._addDataFromTop)(
+				data, toTop, toTopWithAnimate
+			);
 		},
 		// 重新设置列表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
 		resetTotalData(data) {
@@ -547,7 +543,7 @@ export default {
 				return;
 			}
 			this._doCheckScrollViewShouldFullHeight(newVal);
-			if(!this.realTotalData.length && !newVal.length){
+			if (!this.realTotalData.length && !newVal.length) {
 				eventThrow = false;
 			}
 			this.realTotalData = newVal;
@@ -613,7 +609,7 @@ export default {
 					this.totalData = [...this.totalData, ...newVal];
 					// 此处是为了解决在微信小程序中,在某些情况下滚动到底部加载更多后滚动位置直接变为最底部的问题,因此需要通过代码强制滚动回加载更多前的位置
 					// #ifdef MP-WEIXIN
-					if (!this.isIos && !this.refresherOnly && !this.usePageScroll && newVal.length) {
+					if (!this.isIos && !this.isOnly && !this.usePageScroll && newVal.length) {
 						this.loadingMoreTimeStamp = u.getTime();
 						this.$nextTick(() => {
 							this.scrollToY(currentScrollTop);
@@ -659,6 +655,20 @@ export default {
 			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);

+ 1 - 1
uni_modules/z-paging/components/z-paging/js/modules/empty.js

@@ -108,7 +108,7 @@ export default {
 		},
 		// 是否展示空数据图
 		showEmpty() {
-			if (this.refresherOnly || this.hideEmptyView || this.realTotalData.length) return false;
+			if (this.isOnly || this.hideEmptyView || this.realTotalData.length) return false;
 			if (this.autoHideEmptyViewWhenLoading) {
 				if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
 			} else {

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

@@ -255,10 +255,10 @@ export default {
 			}
 			// emit scrolltolower
 			this._emitScrollEvent('scrolltolower');
-			// 如果是只使用下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则return,不触发内部加载更多逻辑
-			if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
+			// 如果是只使用布局或下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则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.refresherOnly && !this.usePageScroll) {
+			if (!this.isIos && !this.isOnly && !this.usePageScroll) {
 				const currentTimestamp = u.getTime();
 				// 在非ios平台+scroll-view中节流处理
 				if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
@@ -352,7 +352,7 @@ export default {
 		_showLoadingMore(type) {
 			if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
 			if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) || 
-			(!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.refresherOnly) {
+			(!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.isOnly) {
 				return false;
 			}
 			if (this.useChatRecordMode && type !== 'Loading') return false;

+ 34 - 3
uni_modules/z-paging/components/z-paging/js/modules/nvue.js

@@ -67,6 +67,7 @@ export default {
 			nShowRefresherRevealHeight: 0,
 			nOldShowRefresherRevealHeight: -1,
 			nRefresherWidth: u.rpx2px(750),
+			nListHeight: 0,
 			nF2Opacity: 0
 		}
 	},
@@ -130,10 +131,39 @@ export default {
 		// 列表滚动时触发
 		_nOnScroll(e) {
 			this.$emit('scroll', e);
-			const contentOffsetY = -e.contentOffset.y;
-			this.oldScrollTop = contentOffsetY;
+			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(contentOffsetY, contentOffsetY - 1);
+			this._checkShouldShowBackToTop(scrollTop, scrollTop - 1);
 		},
 		// 列表滚动结束
 		_nOnScrollend(e) {
@@ -146,6 +176,7 @@ export default {
 			// 判断是否滚动到底部了
 			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');
 					}

+ 9 - 5
uni_modules/z-paging/components/z-paging/js/modules/refresher.js

@@ -298,7 +298,7 @@ export default {
 			return u.addUnit(this.refresherThreshold, this.unit);
 		},
 		finalRefresherEnabled() {
-			if (this.useChatRecordMode) return false;
+			if (this.layoutOnly || this.useChatRecordMode) return false;
 			if (this.privateRefresherEnabled === -1) return this.refresherEnabled;
 			return this.privateRefresherEnabled === 1;
 		},
@@ -383,6 +383,10 @@ export default {
 		updateCustomRefresherHeight() {
 			u.delay(() => this.$nextTick(this._updateCustomRefresherHeight));
 		},
+		// 进入二楼
+		goF2() {
+			this._handleGoF2();
+		},
 		// 关闭二楼
 		closeF2() {
 			this._handleCloseF2();
@@ -546,7 +550,7 @@ export default {
 			this.refresherReachMaxAngle = true;
 			this.isTouchEnded = true;
 			const refresherThreshold = this.finalRefresherThreshold;
-			if (moveDis >= refresherThreshold && (this.refresherStatus === Enum.Refresher.ReleaseToRefresh || this.refresherStatus === Enum.Refresher.GoF2)) {
+			if (moveDis >= refresherThreshold && [Enum.Refresher.ReleaseToRefresh, Enum.Refresher.GoF2].indexOf(this.refresherStatus) >= 0) {
 				// 如果是松手进入二楼状态,则触发进入二楼
 				if (this.refresherStatus === Enum.Refresher.GoF2) {
 					this._handleGoF2();
@@ -615,8 +619,8 @@ export default {
 		// 下拉刷新结束
 		_refresherEnd(shouldEndLoadingDelay = true, fromAddData = false, isUserPullDown = false, setLoading = true) {
 			if (this.loadingType === Enum.LoadingType.Refresher) {
-				// 计算当前下拉刷新结束需要延迟的时间
-				const refresherCompleteDelay = (fromAddData && (isUserPullDown || this.showRefresherWhenReload)) ? this.refresherCompleteDelay : 0;
+				// 计算当前下拉刷新结束需要延迟的时间(用户主动下拉刷新或reload时显示下拉刷新view才需要计算延迟时间)
+				const refresherCompleteDelay = (fromAddData && (isUserPullDown || this.finalShowRefresherWhenReload)) ? this.refresherCompleteDelay : 0;
 				// 如果延迟时间大于0,则展示刷新结束状态,否则直接展示默认状态
 				const refresherStatus = refresherCompleteDelay > 0 ? Enum.Refresher.Complete : Enum.Refresher.Default;
 				if (this.finalShowRefresherWhenReload) {
@@ -793,7 +797,7 @@ export default {
 		// 判断touch手势是否要触发
 		_touchDisabled() {
 			const checkOldScrollTop = this.oldScrollTop > 5;
-			return this.loading || this.isRefresherInComplete || this.useChatRecordMode || !this.refresherEnabled || !this.useCustomRefresher ||(this.usePageScroll && this.useCustomRefresher && this.pageScrollTop > 10) || (!(this.usePageScroll && this.useCustomRefresher) && checkOldScrollTop);
+			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高度

+ 82 - 16
uni_modules/z-paging/components/z-paging/js/modules/scroller.js

@@ -48,6 +48,11 @@ export default {
 			type: String,
 			default: u.gc('scrollIntoView', '')
 		},
+		// z-paging是否使用swiper-item或其他父组件包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,scrollIntoViewById和scrollIntoViewByIndex因无法获取节点信息导致滚动到指定view无效的问题
+		inSwiperSlot: {
+			type: Boolean,
+			default: false
+		},
 	},
 	data() {
 		return {
@@ -63,6 +68,9 @@ export default {
 			privateScrollWithAnimation: -1,
 			cacheScrollNodeHeight: -1,
 			superContentHeight: 0,
+			lastScrollHeight: 0,
+			lastScrollDirection: '',
+			setContentHeightPending: false
 		}
 	},
 	watch: {
@@ -251,7 +259,7 @@ export default {
 		_onScrollToLower(e) {
 			(!e.detail || !e.detail.direction || e.detail.direction === 'bottom') 
 			&& this.toBottomLoadingMoreEnabled
-			&& this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom')
+			&& this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom');
 		},
 		// 滚动到顶部
 		_scrollToTop(animate = true, isPrivate = true) {
@@ -299,6 +307,13 @@ export default {
 				this.scrollTop = 0;
 				this.oldScrollTop = this.scrollTop;
 			});
+			u.delay(() => {
+				this.scrollTop = this.oldScrollTop;
+				this.$nextTick(() => {
+					this.scrollTop = 0;
+					this.oldScrollTop = this.scrollTop;
+				});
+			}, 500)
 		},
 		// 滚动到底部
 		async _scrollToBottom(animate = true) {
@@ -367,11 +382,29 @@ export default {
 					}
 					return;
 					// #endif
-					this._getNodeClientRect('#' + sel.replace('#', ''), this.$parent).then((node) => {
+					// 获取指定view的节点信息
+					let inDom = false;
+					// 在vue3+(微信小程序或QQ小程序)中,无法获取节点信息导致滚动到指定view无效的问题
+					// 通过uni.createSelectorQuery().in(this.$parent)来解决此问题
+					// #ifdef VUE3
+					// #ifdef MP-WEIXIN || MP-QQ
+					if (this.inSwiperSlot) {
+						inDom = this.$parent;
+					}
+					// #endif
+					// #endif
+					this._getNodeClientRect('#' + sel.replace('#', ''), inDom).then((node) => {
 						if (node) {
-							let nodeTop = node[0].top;
-							this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
-							finishCallback && finishCallback();
+							// 获取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}的节点信息,请检查!`);
 						}
 					});
 				});
@@ -425,25 +458,56 @@ export default {
 		// scroll-view滚动中
 		_scroll(e) {
 			this.$emit('scroll', e);
-			const { scrollTop, scrollLeft } = e.detail;
+			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);
+			// 排除快手的情况,因为在快手安卓中双重检测会导致滚动到底部事件多次触发
+			// #ifndef MP-KUAISHOU
+			if (!this.isIos) {
+				// 滚动区域内容的总高度 - 当前滚动的scrollTop = 当前滚动区域的顶部与内容底部的距离
+				const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
+				this._checkScrolledToBottom(scrollDiff);
+			}
+			// #endif
 		},
 		// emit scrolltolower/scrolltoupper事件
 		_emitScrollEvent(type) {
-		    const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower';
-		    const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion
-		        ? reversedType
-		        : type;
-		    
-		    this.$emit(eventType);
+			const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower';
+			const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion ? reversedType : type;
+			this.$emit(eventType);
 		},
 		// 更新内置的scroll-view是否启用滚动动画
 		_updatePrivateScrollWithAnimation(animate) {
@@ -521,7 +585,9 @@ export default {
 			this._doCheckScrollViewShouldFullHeight(this.realTotalData);
 			const node = `.zp-page-${type}`;
 			const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
-			let safeAreaInsetBottomAdd = this.safeAreaInsetBottom;
+			// 是否设置底部安全区域间距,仅当开启底部安全区域并且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

+ 2 - 18
uni_modules/z-paging/components/z-paging/js/modules/virtual-list.js

@@ -79,7 +79,7 @@ export default {
 			type: String,
 			default: u.gc('virtualCellIdPrefix', '')
 		},
-		// 虚拟列表是否使用swiper-item包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
+		// 虚拟列表是否使用swiper-item或其他父组件包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
 		// 仅vue3+(微信小程序或QQ小程序)+非内置列表写法虚拟列表有效,其他情况此属性设置任何值都无效,所以如果您在swiper-item内使用z-paging的非内置虚拟列表写法,将此属性设置为true即可
 		virtualInSwiperSlot: {
 			type: Boolean,
@@ -89,7 +89,6 @@ export default {
 	data() {
 		return {
 			virtualListKey: u.getInstanceId(),
-			virtualPageHeight: 0,
 			virtualCellHeight: 0,
 			virtualScrollTimeStamp: 0,
 			
@@ -108,7 +107,6 @@ export default {
 				fixed: 0,
 				dynamic: 0
 			},
-			pagingOrgTop: -1,
 			updateVirtualListFromDataChange: false
 		}
 	},
@@ -149,7 +147,7 @@ export default {
 			return this.cellKeyName;
 		},
 		finalVirtualPageHeight(){
-			return this.virtualPageHeight > 0 ? this.virtualPageHeight : this.windowHeight;
+			return this.scrollViewHeight > 0 ? this.scrollViewHeight : this.windowHeight;
 		},
 		finalFixedCellHeight() {
 			return u.convertToPx(this.fixedCellHeight);
@@ -271,20 +269,6 @@ export default {
 			}
 			// #endif
 		},
-		// 初始化虚拟列表
-		_virtualListInit() {
-			this.$nextTick(() => {
-				u.delay(() => {
-					// 获取虚拟列表滚动区域的高度
-					this._getNodeClientRect('.zp-scroll-view').then(node => {
-						if (node) {
-							this.pagingOrgTop = node[0].top;
-							this.virtualPageHeight = node[0].height;
-						}
-					});
-				});
-			})
-		},
 		// cellHeightMode为fixed时获取第一个cell高度
 		_updateFixedCellHeight() {
 			if (!this.finalFixedCellHeight) {

+ 1 - 1
uni_modules/z-paging/components/z-paging/js/z-paging-constant.js

@@ -2,7 +2,7 @@
 
 export default {
 	// 当前版本号
-	version: '2.8.6',
+	version: '2.8.8',
 	// 延迟操作的通用时间
 	delayTime: 100,
 	// 请求失败时候全局emit使用的key

+ 52 - 29
uni_modules/z-paging/components/z-paging/js/z-paging-main.js

@@ -60,6 +60,8 @@ export default {
 			checkScrolledToBottomTimeOut: null,
 			cacheTopHeight: -1,
 			statusBarHeight: systemInfo.statusBarHeight,
+			scrollViewHeight: 0,
+			pagingOrgTop: -1,
 
 			// --------------状态&判断---------------
 			insideOfPaging: -1,
@@ -94,6 +96,11 @@ export default {
 			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,
@@ -184,6 +191,16 @@ export default {
 			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,
@@ -192,7 +209,7 @@ export default {
 	},
 	created() {
 		// 组件创建时,检测是否开始加载状态
-		if (this.createdReload && !this.refresherOnly && this.auto) {
+		if (this.createdReload && !this.isOnly && this.auto) {
 			this._startLoading();
 			this.$nextTick(this._preReload);
 		}
@@ -201,7 +218,7 @@ export default {
 		this.active = true;
 		this.wxsPropType = u.getTime().toString();
 		this.renderJsIgnore;
-		if (!this.createdReload && !this.refresherOnly && this.auto) {
+		if (!this.createdReload && !this.isOnly && this.auto) {
 			// 开始预加载
 			u.delay(() => this.$nextTick(this._preReload), 0);
 		}
@@ -211,20 +228,20 @@ export default {
 		// #ifdef H5 || MP
 		delay = c.delayTime;
 		// #endif
+		this.systemInfo = u.getSystemInfoSync();
 		this.$nextTick(() => {
 			// 初始化systemInfo
 			this.systemInfo = u.getSystemInfoSync();
 			// 初始化z-paging高度
 			!this.usePageScroll && this.autoHeight  && this._setAutoHeight();
-			// #ifdef MP-KUAISHOU
-			this._setFullScrollViewInHeight();
-			// #endif
 			this.loaded = true;
 			u.delay(() => {
 				// 更新fixed模式下z-paging的布局,主要是更新windowTop、windowBottom
 				this.updateFixedLayout();
 				// 更新缓存中z-paging整个内容容器高度
 				this._updateCachedSuperContentHeight();
+				// 更新z-paging中scroll-view高度
+				this._updateScrollViewHeight();
 			});
 		})
 		// 初始化页面滚动模式下slot="top"、slot="bottom"高度
@@ -237,8 +254,10 @@ export default {
 				this.isTouchmoving = true;
 			})
 		}
-		// 监听uni.$emit中全局emit的complete error等事件
-		this._onEmit();
+		if (!this.layoutOnly) {
+			// 监听uni.$emit中全局emit的complete error等事件
+			this._onEmit();
+		}
 		// #ifdef APP-NVUE
 		if (!this.isIos && !this.useChatRecordMode) {
 			this.nLoadingMoreFixedHeight = true;
@@ -246,10 +265,6 @@ export default {
 		// 在nvue中更新nvue下拉刷新view容器的宽度,而不是写死默认的750rpx,需要考虑列表宽度不是铺满屏幕的情况
 		this._nUpdateRefresherWidth();
 		// #endif
-		// #ifndef APP-NVUE
-		// 虚拟列表模式时,初始化数据
-		this.finalUseVirtualList && this._virtualListInit();
-		// #endif
 		// #ifndef APP-PLUS
 		this.$nextTick(() => {
 			// 非app平台中,在通过获取css设置的底部安全区域占位view高度设置bottom距离后,更新页面滚动底部高度
@@ -320,7 +335,10 @@ export default {
 			}
 			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(() => {
@@ -335,19 +353,19 @@ export default {
 		},
 		windowBottom() {
 			if (!this.systemInfo) return 0;
-			let windowBottom = this.systemInfo.windowBottom || 0;
-			// 如果开启底部安全区域适配并且不使用placeholder的形式体现并且不是聊天记录模式(因为聊天记录模式在keyboardHeight计算初已添加了底部安全区域),在windowBottom添加底部安全区域高度
-			if (this.safeAreaInsetBottom && !this.useSafeAreaPlaceholder && !this.useChatRecordMode) {
-				windowBottom += this.safeAreaBottom;
-			}
-			return windowBottom;
+			return this.systemInfo.windowBottom || 0;
 		},
+		// 是否是ios+h5
 		isIosAndH5() {
 			// #ifndef H5
 			return false;
 			// #endif
 			return this.isIos;
-		}
+		},
+		// 是否是只使用基础布局或者只使用下拉刷新
+		isOnly() {
+			return this.layoutOnly || this.refresherOnly;
+		},
 	},
 	methods: {
 		// 当前版本号
@@ -434,20 +452,25 @@ export default {
 				}
 			} catch (e) {}
 		},
-		// #ifdef MP-KUAISHOU
-		// 设置scroll-view内容器的最小高度等于scroll-view的高度(为了解决在快手小程序中内容较少时scroll-view内容器高度无法铺满scroll-view的问题)
-		async _setFullScrollViewInHeight() {
-			try {
-				// 如果需要铺满全屏,则计算当前全屏可是区域的高度
-				const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
-				scrollViewNode && this.$set(this.scrollViewInStyle, 'min-height', scrollViewNode[0].height + 'px');
-			} catch (e) {}
+		// 更新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
+			}
 		},
-		// #endif
 		// 组件销毁后续处理
 		_handleUnmounted() {
 			this.active = false;
-			this._offEmit();
+			if (!this.layoutOnly) {
+				this._offEmit();
+			}
 			// 取消监听键盘高度变化事件(H5、百度小程序、抖音小程序、飞书小程序、QQ小程序、快手小程序不支持)
 			// #ifndef H5 || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU
 			this.useChatRecordMode && uni.offKeyboardHeightChange(this._handleKeyboardHeightChange);

+ 21 - 1
uni_modules/z-paging/components/z-paging/js/z-paging-utils.js

@@ -207,6 +207,25 @@ function deepCopy(obj) {
 	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() {
@@ -298,5 +317,6 @@ export default {
 	addUnit,
 	deepCopy,
 	rpx2px,
-	getSystemInfoSync
+	getSystemInfoSync,
+	useBufferedInsert
 };

+ 156 - 20
uni_modules/z-paging/components/z-paging/z-paging.vue

@@ -4,7 +4,7 @@
   / /_____| |_) | (_| | (_| | | | | | (_| |
  /___|    | .__/ \__,_|\__, |_|_| |_|\__, |
           |_|          |___/         |___/ 
-v2.8.6 (2025-03-17)
+v2.8.8 (2025-08-29)
 @author ZXLee <admin@zxlee.cn>
 -->
 <!-- 文档地址:https://z-paging.zxlee.cn -->
@@ -14,19 +14,21 @@ v2.8.6 (2025-03-17)
 
 <template name="z-paging">
 	<!-- #ifndef APP-NVUE -->
-	<view :class="{'z-paging-content':true,'z-paging-content-full':!usePageScroll,'z-paging-content-fixed':!usePageScroll&&fixed,'z-paging-content-page':usePageScroll,'z-paging-reached-top':renderPropScrollTop<1,'z-paging-use-chat-record-mode':useChatRecordMode}" :style="[finalPagingStyle]">
+	<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>
+		<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom" style="position: absolute"/>
 		<!-- #endif -->
 		<!-- 二楼view -->
 		<view v-if="showF2 && showRefresherF2" @touchmove.stop.prevent class="zp-f2-content" :style="[{'transform': f2Transform, 'transition': `transform .2s linear`, 'height': superContentHeight + 'px', 'z-index': f2ZIndex}]">
 			<slot name="f2"/>
 		</view>
 		<!-- 顶部固定的slot -->
-		<slot v-if="!usePageScroll&&zSlots.top" name="top" />
-		<view class="zp-page-top" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
-			<slot name="top" />
-		</view>
+		<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" />
@@ -58,7 +60,7 @@ v2.8.6 (2025-03-17)
 						:change:prop="pagingWxs.propObserver" :prop="wxsPropType"
 						:data-refresherThreshold="finalRefresherThreshold" :data-refresherF2Enabled="refresherF2Enabled" :data-refresherF2Threshold="finalRefresherF2Threshold" :data-isIos="isIos"
 						:data-loading="loading||isRefresherInComplete" :data-useChatRecordMode="useChatRecordMode" 
-						:data-refresherEnabled="refresherEnabled" :data-useCustomRefresher="useCustomRefresher" :data-pageScrollTop="wxsPageScrollTop"
+						:data-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"
@@ -100,7 +102,7 @@ v2.8.6 (2025-03-17)
 										<view class="zp-list-container" :style="[innerListStyle]">
 											<template v-if="finalUseVirtualList">
 												<view class="zp-list-cell" :style="[innerCellStyle]" :id="`${fianlVirtualCellIdPrefix}-${item[virtualCellIndexKey]}`" v-for="(item,index) in virtualList" :key="item['zp_unique_index']" @click="_innerCellClick(item,virtualTopRangeIndex+index)">
-													<view v-if="useCompatibilityMode">使用兼容模式请在组件源码z-paging.vue第103行中注释这一行,并打开下面一行注释</view>
+													<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>
@@ -140,7 +142,11 @@ v2.8.6 (2025-03-17)
 									<slot v-else-if="loadingStatus===M.Fail&&zSlots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreFail" />
 									<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===M.NoMore&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :zConfig="zLoadMoreConfig" />
 									<!-- #endif -->
-									<view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+									<!-- 底部安全区域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]">
@@ -161,10 +167,37 @@ v2.8.6 (2025-03-17)
 		</view>
 		<!-- 底部固定的slot -->
 		<view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
-			<slot v-if="!usePageScroll&&zSlots.bottom" name="bottom" />
-			<view class="zp-page-bottom" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.bottom" :style="[{'bottom': `${windowBottom}px`}]">
-				<slot name="bottom" />
-			</view>
+			<template v-if="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设置底部安全区域间距) -->
+					<template v-if="safeAreaInsetBottom">
+						<!-- 如果是App,则使用style中的safeAreaBottom设置高度,非APP使用class,因为class中的env(safe-area-inset-bottom)在部分app中无效 -->
+						<!-- #ifdef APP-PLUS -->
+						<view :style="[{height:safeAreaBottom+'px'}]" />
+						<!-- #endif -->
+						<!-- #ifndef APP-PLUS -->
+						<view class="zp-safe-area-inset-bottom" />
+						<!-- #endif -->
+					</template>
+				</view>
+			</template>
+			<!-- 非页面滚动底部安全区域占位(无论slot=bottom是否存在)-->
+			<!-- 如果useSafeAreaPlaceholder开启了并且slot=bottom不存在就不显示这个占位view了,因为此时useSafeAreaPlaceholder会是跟随滚动的状态 -->
+			<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+			<template v-if="safeAreaInsetBottom&&!usePageScroll&&!(finalUseSafeAreaPlaceholder)&&!useChatRecordMode">
+				<!-- 如果是App,则使用style中的safeAreaBottom设置高度,非APP使用class,因为class中的env(safe-area-inset-bottom)在部分app中无效 -->
+				<!-- #ifdef APP-PLUS -->
+				<view :style="[{height:safeAreaBottom+'px'}]" />
+				<!-- #endif -->
+				<!-- #ifndef APP-PLUS -->
+				<view class="zp-safe-area-inset-bottom" />
+				<!-- #endif -->
+			</template>
+			
 			<!-- 聊天记录模式底部占位 -->
 			<template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
 				<view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
@@ -183,7 +216,7 @@ v2.8.6 (2025-03-17)
 	</view>
 	<!-- #endif -->
 	<!-- #ifdef APP-NVUE -->
-	<component ref="z-paging-content" :is="finalNvueSuperListIs" :style="[finalPagingStyle]" :class="{'z-paging-content-fixed':fixed&&!usePageScroll}" :scrollable="false">
+	<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"/>
@@ -201,7 +234,9 @@ v2.8.6 (2025-03-17)
 			<view v-if="zSlots.left" class="zp-page-left">
 				<slot name="left" />
 			</view>
-			<component :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},chatRecordRotateStyle]" :alwaysScrollableVertical="true"
+			<!-- 因在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"
@@ -253,7 +288,7 @@ v2.8.6 (2025-03-17)
 					<slot name="loading" />
 				</component>
 				<!-- 上拉加载更多view -->
-				<component :is="nViewIs" v-if="!refresherOnly&&loadingMoreEnabled&&!showEmpty">
+				<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]">
@@ -271,7 +306,10 @@ v2.8.6 (2025-03-17)
 						<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
 						<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
 						<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
-						<view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
+						<!-- 底部安全区域useSafeAreaPlaceholder模式占位,此时占位不再固定在底部而是跟随页面一起滚动 -->
+						<!-- 如果底部slot=bottom存在,占位区域会插入在slot=bottom下方,不再跟随页面滚动,因此这里就没必要显示了 -->
+						<!-- 聊天记录模式因为列表倒置,此处不需要显示底部安全区域,另行处理 -->
+						<view v-if="safeAreaInsetBottom&&finalUseSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
 					</view>
 				</component>
 				<!-- 空数据图 -->
@@ -286,6 +324,94 @@ v2.8.6 (2025-03-17)
 				</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>
@@ -293,6 +419,11 @@ v2.8.6 (2025-03-17)
 		<!-- 底部固定的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'}]" />
@@ -327,11 +458,14 @@ v2.8.6 (2025-03-17)
 	 * @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
@@ -451,7 +585,8 @@ v2.8.6 (2025-03-17)
 	 * @property {String} virtualCellIdPrefix 虚拟列表cell id的前缀
 	 * @property {Boolean} useInnerList 是否在z-paging内部循环渲染列表(使用内置列表),默认为false
 	 * @property {Boolean} forceCloseInnerList 强制关闭inner-list,默认为false
-	 * @property {Boolean} virtualInSwiperSlot 虚拟列表是否使用swiper-item包裹,默认为false
+	 * @property {Boolean} virtualInSwiperSlot 虚拟列表是否使用swiper-item或其他父组件包裹,默认为false
+	 * @property {Boolean} inSwiperSlot z-paging是否使用swiper-item或其他父组件包裹,默认为否
 	 * @property {String} cellKeyName 内置列表cell的key名称(仅nvue有效)
 	 * @property {Object} innerListStyle innerList样式
 	 * @property {Object} innerCellStyle innerCell样式
@@ -515,7 +650,8 @@ v2.8.6 (2025-03-17)
 	 * @event {Function} scrolltoupper z-paging内置的scroll-view/list-view/waterfall滚动顶部时触发
 	 * @event {Function} scrollend z-paging内置的list滚动结束时触发
 	 * @event {Function} contentHeightChanged z-paging中内容高度改变时触发
-	 * @event {Function} touchDirectionChange 监听列表触摸方向改变
+	 * @event {Function} touchDirectionChange 监听列表触摸方向改变(nvue无效)
+	 * @event {Function} scrollDirectionChange 监听列表滚动方向改变(页面滚动无效)
 	 * @example <z-paging ref="paging" v-model="dataList" @query="queryList"></z-paging>
 	 */
 	export default {

+ 41 - 43
uni_modules/z-paging/package.json

@@ -2,7 +2,7 @@
   "id": "z-paging",
   "name": "z-paging",
   "displayName": "【z-paging下拉刷新、上拉加载】高性能,全平台兼容。支持虚拟列表,分页全自动处理",
-  "version": "2.8.6",
+  "version": "2.8.8",
   "description": "超简单、低耦合!使用wxs+renderjs实现。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、无闪动聊天分页、本地分页、国际化等数百项配置",
   "keywords": [
     "下拉刷新",
@@ -13,7 +13,8 @@
 ],
   "repository": "https://github.com/SmileZXLee/uni-z-paging",
   "engines": {
-    "HBuilderX": "^3.0.7"
+    "HBuilderX": "^3.0.7",
+    "uni-app": "^4.07"
   },
   "dcloudext": {
     "sale": {
@@ -33,55 +34,52 @@
       "permissions": "无"
     },
     "npmurl": "https://www.npmjs.com/package/z-paging",
-    "type": "component-vue"
+    "type": "component-vue",
+    "darkmode": "√",
+    "i18n": "√",
+    "widescreen": "√"
   },
   "uni_modules": {
     "dependencies": [],
     "encrypt": [],
     "platforms": {
       "cloud": {
-        "tcb": "y",
-        "aliyun": "y",
-        "alipay": "n"
+        "tcb": "",
+        "aliyun": "",
+        "alipay": ""
       },
       "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"
+        "uni-app": {
+          "vue": {
+            "vue2": "√",
+            "vue3": "√"
+          },
+          "web": {
+            "safari": "√",
+            "chrome": "√"
+          },
+          "app": {
+            "vue": "√",
+            "nvue": "√",
+            "android": "√",
+            "ios": "√",
+            "harmony": "√"
+          },
+          "mp": {
+            "weixin": "√",
+            "alipay": "√",
+            "toutiao": "√",
+            "baidu": "√",
+            "kuaishou": "√",
+            "jd": "√",
+            "harmony": "-",
+            "qq": "√",
+            "lark": "√"
+          },
+          "quickapp": {
+            "huawei": "√",
+            "union": "-"
+          }
         }
       }
     }

+ 2 - 2
uni_modules/z-paging/readme.md

@@ -4,7 +4,7 @@
     <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.6-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)
+[![version](https://img.shields.io/badge/version-2.8.8-blue)](https://github.com/SmileZXLee/uni-z-paging) [![license](https://img.shields.io/github/license/SmileZXLee/uni-z-paging)](https://en.wikipedia.org/wiki/MIT_License)
 <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)  
@@ -19,7 +19,7 @@
 * 【低耦合,低侵入】分页自动管理。在page中无需处理任何分页相关逻辑,无需在data中定义任何分页相关变量,全由z-paging内部处理。
 * 【超灵活,支持各种类型自定义】支持自定义下拉刷新,自定义上拉加载更多等各种自定义效果;支持使用内置自动分页,同时也支持通过监听下拉刷新和滚动到底部事件自行处理;支持使用自带全屏布局规范,同时也支持将z-paging自由放在任意容器中。
 * 【功能丰富】支持国际化,支持自定义且自动管理空数据图,支持主题模式切换,支持本地分页,支持无闪动聊天分页模式,支持展示最后更新时间,支持吸顶效果,支持内部scroll-view滚动与页面滚动,支持一键滚动到顶部,支持下拉进入二楼等诸多功能。
-* 【全平台兼容】支持vue&nvue,vue2&vue3,js&ts,支持h5、app、鸿蒙Next及各家小程序。
+* 【全平台兼容】支持vue&nvue,vue2&vue3,js&ts,支持h5、app、鸿蒙Next及各家小程序。
 * 【高性能】在app-vue、h5、微信小程序、QQ小程序上使用wxs+renderjs在视图层实现下拉刷新;支持虚拟列表,轻松渲染百万级列表数据!
 
 *** 

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

@@ -5,7 +5,7 @@ declare interface ZPagingCellProps {
   /**
    * z-paging-cell样式
    */
-  cellStyle?: Record<string, any>
+  cellStyle?: Partial<CSSStyleDeclaration>
 }
 
 // ****************************** Slots ******************************

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

@@ -32,23 +32,23 @@ declare interface ZPagingEmptyViewProps {
    * 空数据图样式,可设置空数据view的top等
    * - 如果空数据图不是fixed布局,则此处是`margin-top`
    */
-  emptyViewStyle?: Record<string, any>;
+  emptyViewStyle?: Partial<CSSStyleDeclaration>;
 
   /**
    * 空数据图img样式
    */
-  emptyViewImgStyle?: Record<string, any>;
+  emptyViewImgStyle?: Partial<CSSStyleDeclaration>;
 
   /**
    * 空数据图描述文字样式
    */
-  emptyViewTitleStyle?: Record<string, any>;
+  emptyViewTitleStyle?: Partial<CSSStyleDeclaration>;
 
   /**
    * 空数据图重新加载按钮样式
    * @since 1.6.7
    */
-  emptyViewReloadStyle?: Record<string, any>;
+  emptyViewReloadStyle?: Partial<CSSStyleDeclaration>;
 
   /**
    * 是否显示空数据图重新加载按钮(无数据时)

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

@@ -60,7 +60,7 @@ declare interface ZPagingSwiperItemProps {
   /**
    * innerList样式
    */
-  innerListStyle?: Record<string, any>
+  innerListStyle?: Partial<CSSStyleDeclaration>
 }
 
 // ****************************** Methods ******************************

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

@@ -17,7 +17,7 @@ declare interface ZPagingSwiperProps {
   /**
    * z-paging-swiper样式
    */
-  swiperStyle?: Record<string, any>
+  swiperStyle?: Partial<CSSStyleDeclaration>
 }
 
 

+ 74 - 18
uni_modules/z-paging/types/comps/z-paging.d.ts

@@ -33,6 +33,11 @@ declare global {
      * 列表触摸的方向,top代表用户将列表向上移动(scrollTop不断减小),bottom代表用户将列表向下移动(scrollTop不断增大)
      */
     type TouchDirection = 'top' | 'bottom';
+
+    /**
+     * 列表滚动的方向,top代表用户将列表向上移动(scrollTop不断减小),bottom代表用户将列表向下移动(scrollTop不断增大)
+     */
+    type ScrollDirection = 'top' | 'bottom';
   }
 
   namespace ZPagingParams {
@@ -266,7 +271,13 @@ declare interface ZPagingProps {
   /**
    * 设置z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替。
    */
-  pagingStyle?: Record<string, any>
+  pagingStyle?: Partial<CSSStyleDeclaration>
+
+  /**
+   * 设置z-paging的class,优先级低于pagingStyle和height、width、maxWidth、bgColor
+   * @since 2.8.7
+   */
+  pagingClass?: string | string[] | Record<string, boolean>
 
   /**
    * z-paging的高度,优先级低于paging-style中设置的height
@@ -299,6 +310,21 @@ declare interface ZPagingProps {
    */
   watchTouchDirectionChange?: boolean
 
+  /**
+   * 是否监听列表滚动方向改变
+   * @default false
+   * @since 2.8.7
+   */
+  watchScrollDirectionChange?: boolean
+  
+  /**
+   * 是否只使用基础布局
+   * - 设置为true后将关闭mounted自动请求数据、关闭下拉刷新和滚动到底部加载更多,强制隐藏空数据图
+   * @default false
+   * @since 2.8.7
+   */
+  layoutOnly?: boolean
+
   /**
    * 调用complete后延迟处理的时间,单位为毫秒,优先级高于min-delay
    * @default 0
@@ -460,18 +486,18 @@ declare interface ZPagingProps {
   /**
    * 自定义下拉刷新中左侧图标的样式
    */
-  refresherImgStyle?: Record<string, any>
+  refresherImgStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 自定义下拉刷新中右侧状态描述文字的样式
    */
-  refresherTitleStyle?: Record<string, any>
+  refresherTitleStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 自定义下拉刷新中右侧最后更新时间文字的样式
    * - show-refresher-update-time为true时有效
    */
-  refresherUpdateTimeStyle?: Record<string, any>
+  refresherUpdateTimeStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 是否实时监听下拉刷新中进度,并通过@refresherTouchmove传递给父组件
@@ -712,18 +738,18 @@ declare interface ZPagingProps {
    * 自定义底部加载更多样式;如:{'background':'red'} 
    * - 此属性无法修改文字样式,修改文字样式请使用loading-more-title-custom-style
    */
-  loadingMoreCustomStyle?: Record<string, any>
+  loadingMoreCustomStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 自定义底部加载更多文字样式;如:{'color':'red'}
    * @since 2.1.7
    */
-  loadingMoreTitleCustomStyle?: Record<string, any>
+  loadingMoreTitleCustomStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 自定义底部加载更多加载中动画样式
    */
-  loadingMoreLoadingIconCustomStyle?: Record<string, any>
+  loadingMoreLoadingIconCustomStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 自定义底部加载更多加载中动画图标类型
@@ -826,7 +852,7 @@ declare interface ZPagingProps {
   /**
    * 自定义底部没有更多数据的分割线样式
    */
-  loadingMoreNoMoreLineCustomStyle?: Record<string, any>
+  loadingMoreNoMoreLineCustomStyle?: Partial<CSSStyleDeclaration>
 
   // ******************** 空数据与加载失败配置 ********************
   /**
@@ -889,28 +915,28 @@ declare interface ZPagingProps {
   /**
    * 空数据图父view样式
    */
-  emptyViewSuperStyle?: Record<string, any>
+  emptyViewSuperStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 空数据图样式,可设置空数据view的top等,如::empty-view-style="{'top':'100rpx'}"
    */
-  emptyViewStyle?: Record<string, any>
+  emptyViewStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 空数据图img样式
    */
-  emptyViewImgStyle?: Record<string, any>
+  emptyViewImgStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 空数据图描述文字样式
    */
-  emptyViewTitleStyle?: Record<string, any>
+  emptyViewTitleStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 空数据图重新加载按钮样式
    * @since 1.6.7
    */
-  emptyViewReloadStyle?: Record<string, any>
+  emptyViewReloadStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * 是否显示空数据图重新加载按钮(无数据时)
@@ -1014,7 +1040,7 @@ declare interface ZPagingProps {
   /**
    * 点击返回顶部按钮的自定义样式
    */
-  backToTopStyle?: Record<string, any>
+  backToTopStyle?: Partial<CSSStyleDeclaration>
 
   // ******************** 虚拟列表&内置列表配置 ********************
   /**
@@ -1090,13 +1116,21 @@ declare interface ZPagingProps {
   forceCloseInnerList?: boolean
 
   /**
-   * 虚拟列表是否使用swiper-item包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
+   * 虚拟列表是否使用swiper-item或其他父组件包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
    * - 仅vue3+(微信小程序或QQ小程序)+非内置列表写法虚拟列表有效,其他情况此属性设置任何值都无效,所以如果您在swiper-item内使用z-paging的非内置虚拟列表写法,将此属性设置为true即可
    * @default false
    * @since 2.8.6
    */
   virtualInSwiperSlot?: boolean
 
+  /**
+   * z-paging是否使用swiper-item或其他父组件包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,scrollIntoViewById和scrollIntoViewByIndex因无法获取节点信息导致滚动到指定view无效的问题
+   * - 仅vue3+(微信小程序或QQ小程序)且需要调用scrollIntoViewById或scrollIntoViewByIndex方法时有效,其他情况此属性设置任何值都无效
+   * @default false
+   * @since 2.8.8
+   */
+  inSwiperSlot?: boolean
+
   /**
    * 内置列表cell的key名称(仅nvue有效,在nvue中开启use-inner-list时必须填此项)
    * @since 2.2.7
@@ -1106,13 +1140,13 @@ declare interface ZPagingProps {
   /**
    * innerList样式
    */
-  innerListStyle?: Record<string, any>
+  innerListStyle?: Partial<CSSStyleDeclaration>
 
   /**
    * innerCell样式
    * @since 2.2.8
    */
-  innerCellStyle?: Record<string, any>
+  innerCellStyle?: Partial<CSSStyleDeclaration>
 
   // ******************** 本地分页配置 ********************
   /**
@@ -1541,6 +1575,15 @@ declare interface ZPagingProps {
    * @since 2.3.0
    */
   onTouchDirectionChange?: (direction: ZPagingEnums.TouchDirection) => void
+
+  /**
+   * 监听列表滚动方向改变
+   * - 页面滚动无效
+   * - 必须同时设置:watch-scroll-direction-change="true"
+   * @param direction 列表滚动的方向,top代表用户将列表向上移动(scrollTop不断减小),bottom代表用户将列表向下移动(scrollTop不断增大)
+   * @since 2.8.7
+   */
+  onScrollDirectionChange?: (direction: ZPagingEnums.ScrollDirection) => void
 }
 
 
@@ -1840,6 +1883,13 @@ declare interface _ZPagingRef<T = any> {
    * @since 2.6.1
    */
   updateCustomRefresherHeight: () => void;
+  
+  /**
+   * 手动进入二楼
+   *
+   * @since 2.8.7
+   */
+  goF2: () => void;
 
   /**
    * 手动关闭二楼
@@ -1954,6 +2004,13 @@ declare interface _ZPagingRef<T = any> {
    */
   addChatRecordData: (data: _Arrayable<T>, scrollToBottom?: boolean, animate?: boolean) => void;
 
+  /**
+   * 手动添加键盘高度变化监听
+   * 
+   * @since 2.8.7
+   */
+  addKeyboardHeightChangeListener: () => void;
+
   // ******************** 滚动到指定位置方法 ********************
   /**
    * 滚动到顶部
@@ -1971,7 +2028,6 @@ declare interface _ZPagingRef<T = any> {
 
   /**
    * 滚动到指定view
-   * - vue中有效,若此方法无效,请使用scrollIntoViewByNodeTop
    *
    * @param id 需要滚动到的view的id值,不包含"#"
    * @param [offset=0] 偏移量,单位为px