Browse Source

feat: infiniteloading

yangxiaolu3 3 years ago
parent
commit
8a6cf09d14

+ 17 - 1
src/config.json

@@ -715,7 +715,23 @@
           "desc": "列表滚动到底部自动加载更多数据",
           "sort": 15,
           "show": true,
-          "author": "yangxiaolu"
+          "author": "yangxiaolu",
+          "exportEmpty": false,
+          "exportEmptyTaro": false
+        },
+        {
+          "version": "3.0.0",
+          "name": "PullRefresh",
+          "taro": true,
+          "tarodoc": true,
+          "type": "component",
+          "cName": "下拉刷新",
+          "desc": "在列表中通过手指下拉刷新加载新内容的交互操作",
+          "sort": 16,
+          "show": true,
+          "author": "yangxiaolu",
+          "exportEmpty": false,
+          "exportEmptyTaro": false
         },
         {
           "version": "3.0.0",

+ 80 - 76
src/packages/__VUE/infiniteloading/demo.vue

@@ -1,36 +1,25 @@
 <template>
   <div class="demo">
-    <nut-cell-group :title="translate('basic')">
-      <nut-cell>
+    <nut-tabs v-model="tabsValue" animatedTime="0" @change="chagetabs">
+      <nut-tabpane :title="translate('basic')">
         <ul class="infiniteUl" id="scroll">
-          <nut-infiniteloading container-id="scroll" :use-window="false" :has-more="hasMore" @load-more="loadMore">
-            <li class="infiniteLi" v-for="(item, index) in defultList" :key="index">{{ item }}</li>
-          </nut-infiniteloading>
-        </ul>
-      </nut-cell>
-    </nut-cell-group>
-    <nut-cell-group :title="translate('pullRefresh')">
-      <nut-cell>
-        <ul class="infiniteUl" id="refreshScroll">
           <nut-infiniteloading
-            pull-icon="JD"
-            container-id="refreshScroll"
+            v-model="infinityValue"
+            container-id="scroll"
             :use-window="false"
-            :is-open-refresh="true"
-            :has-more="refreshHasMore"
-            @load-more="refreshLoadMore"
-            @refresh="refresh"
+            :has-more="hasMore"
+            @load-more="loadMore"
           >
-            <li class="infiniteLi" v-for="(item, index) in refreshList" :key="index">{{ item }}</li>
+            <li class="infiniteLi" v-for="(item, index) in defultList" :key="index">{{ item }}</li>
           </nut-infiniteloading>
         </ul>
-      </nut-cell>
-    </nut-cell-group>
-    <nut-cell-group :title="translate('customTxt')">
-      <nut-cell>
+      </nut-tabpane>
+
+      <nut-tabpane :title="translate('customTxt')">
         <ul class="infiniteUl" id="customScroll">
           <nut-infiniteloading
-            load-txt="loading"
+            v-model="infinityValue2"
+            load-txt="Loading..."
             :load-more-txt="translate('none')"
             container-id="customScroll"
             :use-window="false"
@@ -40,8 +29,8 @@
             <li class="infiniteLi" v-for="(item, index) in customList" :key="index">{{ item }}</li>
           </nut-infiniteloading>
         </ul>
-      </nut-cell>
-    </nut-cell-group>
+      </nut-tabpane>
+    </nut-tabs>
   </div>
 </template>
 
@@ -73,72 +62,79 @@ export default createDemo({
   props: {},
   setup() {
     initTranslate();
-    let { proxy } = getCurrentInstance() as any;
+    const letter: any[] = [
+      'A',
+      'B',
+      'C',
+      'D',
+      'E',
+      'F',
+      'G',
+      'H',
+      'IJ',
+      'K',
+      'L',
+      'M',
+      'N',
+      'O',
+      'P',
+      'Q',
+      'R',
+      'S',
+      'T',
+      'U',
+      'V',
+      'W',
+      'X',
+      'Y',
+      'Z'
+    ];
 
     const hasMore = ref(true);
     const customHasMore = ref(true);
     const refreshHasMore = ref(true);
 
     const data = reactive({
+      tabsValue: 0,
+      infinityValue: false,
+      infinityValue2: false,
       defultList: [],
       customList: [],
       refreshList: []
     });
 
-    const loadMore = (done) => {
-      setTimeout(() => {
-        const curLen = data.defultList.length;
-
-        for (let i = curLen; i < curLen + 10; i++) {
-          data.defultList.push(`${i}`);
-        }
+    let cycle = 0;
+    let cycle2 = 0;
 
-        if (data.defultList.length > 30) hasMore.value = false;
-
-        done();
-      }, 500);
-    };
-
-    const customLoadMore = (done) => {
+    const loadMore = () => {
       setTimeout(() => {
-        const curLen = data.customList.length;
-        for (let i = curLen; i < curLen + 10; i++) {
-          data.customList.push(`${i}`);
-        }
-        if (data.customList.length > 30) customHasMore.value = false;
-        done();
-      }, 500);
+        data.defultList = data.defultList.concat(letter);
+        cycle++;
+        if (cycle > 2) hasMore.value = false;
+        data.infinityValue = false;
+      }, 1000);
     };
 
-    const refreshLoadMore = (done) => {
+    const customLoadMore = () => {
       setTimeout(() => {
-        const curLen = data.refreshList.length;
-        for (let i = curLen; i < curLen + 10; i++) {
-          data.refreshList.push(`${i}`);
-        }
-        if (data.refreshList.length > 30) refreshHasMore.value = false;
-        done();
-      }, 500);
-    };
+        data.customList = data.defultList.concat(letter);
+        cycle2++;
+        if (cycle2 > 2) customHasMore.value = false;
 
-    const refresh = (done) => {
-      setTimeout(() => {
-        proxy.$toast.text(translate('success'));
-        data.refreshList = data.refreshList.slice(0, 10);
-        refreshHasMore.value = true;
-        done();
+        data.infinityValue2 = false;
       }, 1000);
     };
 
-    const init = () => {
-      for (let i = 0; i < 10; i++) {
-        data.defultList.push(`${i}`);
-        data.customList.push(`${i}`);
-        data.refreshList.push(`${i}`);
-      }
+    const chagetabs = () => {
+      data.defultList = [].concat(letter);
+      data.customList = [].concat(letter);
+      data.infinityValue2 = false;
+      data.infinityValue = false;
+      customHasMore.value = true;
+      hasMore.value = true;
     };
     onMounted(() => {
-      init();
+      chagetabs();
     });
 
     return {
@@ -147,23 +143,31 @@ export default createDemo({
       customHasMore,
       customLoadMore,
       refreshHasMore,
-      refreshLoadMore,
-      refresh,
       translate,
-      ...toRefs(data)
+      ...toRefs(data),
+      chagetabs
     };
   }
 });
 </script>
 
 <style lang="scss" scoped>
+.demo {
+  padding-left: 0px !important;
+  padding-right: 0px !important;
+}
 .nut-theme-dark {
   .infiniteLi {
     color: $dark-color;
   }
 }
+
+.nut-tabpane {
+  padding: 0 !important;
+  padding-left: 16px !important;
+}
 .infiniteUl {
-  height: 300px;
+  height: calc(100vh - 110px);
   width: 100%;
   padding: 0;
   margin: 0;
@@ -171,10 +175,10 @@ export default createDemo({
   overflow-x: hidden;
 }
 .infiniteLi {
-  margin-top: 10px;
   font-size: 14px;
-  color: rgba(100, 100, 100, 1);
-  text-align: center;
+  color: #333;
+  padding: 12px 0;
+  border-bottom: 1px solid #eee;
 }
 
 .loading {

+ 3 - 30
src/packages/__VUE/infiniteloading/index.scss

@@ -1,11 +1,5 @@
 .nut-theme-dark {
   .nut-infiniteloading {
-    .nut-infinite-top {
-      .top-text {
-        color: $dark-color;
-      }
-    }
-
     .nut-infinite-bottom {
       color: $dark-color;
 
@@ -19,33 +13,12 @@
 .nut-infiniteloading {
   display: block;
   width: 100%;
-  .nut-infinite-top {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 100%;
-    overflow: hidden;
-    .top-box {
-      width: 100%;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
 
-      .top-img {
-        width: 28px;
-        height: 24px;
-      }
-      .top-text {
-        font-size: 10px;
-        color: $text-color;
-      }
-    }
-  }
   .nut-infinite-bottom {
     display: block;
     width: 100%;
-    padding-top: 16px;
+    height: 50px;
+    line-height: 50px;
     font-size: $font-size-small;
     color: $infiniteloading-bottom-color;
     text-align: center;
@@ -57,7 +30,7 @@
         height: 24px;
       }
       .bottom-text {
-        font-size: 10px;
+        font-size: 12px;
         color: $text-color;
       }
     }

+ 0 - 228
src/packages/__VUE/infiniteloading/index.taro.vue

@@ -1,228 +0,0 @@
-<template>
-  <Nut-Scroll-View
-    :class="classes"
-    scrollY="true"
-    style="height: 100%"
-    id="scroller"
-    @scrolltolower="lower"
-    @scroll="scroll"
-    @touchstart="touchStart"
-    @touchmove="touchMove"
-    @touchend="touchEnd"
-  >
-    <view class="nut-infinite-top" :style="getStyle">
-      <view class="top-box" id="refreshTop">
-        <nut-icon class="top-img" v-bind="$attrs" :name="pullIcon"></nut-icon>
-        <view class="top-text">{{ pullTxt || translate('pullTxt') }}</view>
-      </view>
-    </view>
-
-    <view class="nut-infinite-container">
-      <slot></slot>
-    </view>
-
-    <view class="nut-infinite-bottom">
-      <template v-if="isInfiniting">
-        <view class="bottom-box">
-          <nut-icon class="bottom-img" v-bind="$attrs" :name="loadIcon"></nut-icon>
-          <view class="bottom-text">{{ loadTxt || translate('loading') }}</view>
-        </view>
-      </template>
-      <template v-else-if="!hasMore">
-        <view class="tips">{{ loadMoreTxt || translate('loadMoreTxt') }}</view>
-      </template>
-    </view>
-  </Nut-Scroll-View>
-</template>
-<script lang="ts">
-import { toRefs, onMounted, reactive, computed, CSSProperties } from 'vue';
-import { createComponent } from '@/packages/utils/create';
-import NutScrollView from '../scrollView/index.taro.vue';
-const { componentName, create, translate } = createComponent('infiniteloading');
-import Icon from '../icon/index.taro.vue';
-import Taro from '@tarojs/taro';
-export default create({
-  props: {
-    hasMore: {
-      type: Boolean,
-      default: true
-    },
-    threshold: {
-      type: Number,
-      default: 200
-    },
-    upperThreshold: {
-      type: Number,
-      default: 40
-    },
-    pullIcon: {
-      type: String,
-      default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
-    },
-    pullTxt: {
-      type: String,
-      default: ''
-    },
-    loadIcon: {
-      type: String,
-      default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
-    },
-    loadTxt: {
-      type: String,
-      default: ''
-    },
-    loadMoreTxt: {
-      type: String,
-      default: ''
-    },
-    useWindow: {
-      type: Boolean,
-      default: true
-    },
-    containerId: {
-      type: String,
-      default: ''
-    },
-    useCapture: {
-      type: Boolean,
-      default: false
-    },
-    isOpenRefresh: {
-      type: Boolean,
-      default: false
-    }
-  },
-  emits: ['scroll-change', 'load-more', 'refresh'],
-  components: {
-    'nut-icon': Icon,
-    NutScrollView
-  },
-  setup(props, { emit, slots }) {
-    const state = reactive({
-      scrollHeight: 0,
-      scrollTop: 0,
-      isInfiniting: false,
-      direction: 'down',
-      isTouching: false,
-      refreshMaxH: 0,
-      y: 0,
-      x: 0,
-      distance: 0
-    });
-
-    const classes = computed(() => {
-      const prefixCls = componentName;
-      return {
-        [prefixCls]: true
-      };
-    });
-
-    const getStyle = computed(() => {
-      const style: CSSProperties = {};
-      return {
-        height: state.distance < 0 ? `0px` : `${state.distance}px`,
-        transition: state.isTouching
-          ? `height 0s cubic-bezier(0.25,0.1,0.25,1)`
-          : `height 0.2s cubic-bezier(0.25,0.1,0.25,1)`
-      };
-    });
-    const getParentElement = (el) => {
-      return Taro.createSelectorQuery().select(!!props.containerId ? `#${props.containerId} #${el}` : `#${el}`);
-    };
-    /** 获取需要滚动的距离 */
-    const getScrollHeight = () => {
-      const parentElement = getParentElement('scroller');
-
-      parentElement
-        .boundingClientRect((rect) => {
-          state.scrollHeight = rect.height;
-        })
-        .exec();
-    };
-
-    /** 滚动到底部 */
-    const lower = () => {
-      if (state.direction == 'up' || !props.hasMore || state.isInfiniting) {
-        return false;
-      } else {
-        state.isInfiniting = true;
-        emit('load-more', infiniteDone);
-      }
-    };
-
-    const scroll = (e) => {
-      // 滚动方向
-      if (e.detail.scrollTop <= 0) {
-        // 滚动到最顶部
-        e.detail.scrollTop = 0;
-      } else if (e.detail.scrollTop >= state.scrollHeight) {
-        // 滚动到最底部
-        e.detail.scrollTop = state.scrollHeight;
-      }
-      if (e.detail.scrollTop > state.scrollTop || e.detail.scrollTop >= state.scrollHeight) {
-        state.direction = 'down';
-      } else {
-        state.direction = 'up';
-      }
-      state.scrollTop = e.detail.scrollTop;
-
-      emit('scroll-change', e.detail.scrollTop);
-    };
-
-    const infiniteDone = () => {
-      state.isInfiniting = false;
-    };
-
-    const touchStart = (event: TouchEvent) => {
-      if (state.scrollTop == 0 && !state.isTouching && props.isOpenRefresh) {
-        state.y = event.touches[0].pageY;
-        state.isTouching = true;
-      }
-    };
-
-    const touchMove = (event: TouchEvent) => {
-      state.distance = event.touches[0].pageY - state.y;
-
-      if (state.distance > 0 && state.isTouching) {
-        event.preventDefault();
-        if (state.distance >= state.refreshMaxH) state.distance = state.refreshMaxH;
-      } else {
-        state.distance = 0;
-        state.isTouching = false;
-      }
-    };
-
-    const touchEnd = () => {
-      if (state.distance < state.refreshMaxH) {
-        state.distance = 0;
-      } else {
-        emit('refresh', refreshDone);
-      }
-    };
-
-    const refreshDone = () => {
-      state.distance = 0;
-      state.isTouching = false;
-    };
-
-    onMounted(() => {
-      state.refreshMaxH = props.upperThreshold;
-      setTimeout(() => {
-        getScrollHeight();
-      }, 200);
-    });
-
-    return {
-      classes,
-      ...toRefs(state),
-      lower,
-      scroll,
-      touchStart,
-      touchMove,
-      touchEnd,
-      getStyle,
-      translate
-    };
-  }
-});
-</script>

+ 42 - 119
src/packages/__VUE/infiniteloading/index.vue

@@ -1,12 +1,5 @@
 <template>
-  <view :class="classes" ref="scroller" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
-    <view class="nut-infinite-top" ref="refreshTop" :style="getStyle">
-      <view class="top-box">
-        <nut-icon class="top-img" v-bind="$attrs" :name="pullIcon"></nut-icon>
-        <view class="top-text">{{ pullTxt || translate('pullTxt') }}</view>
-      </view>
-    </view>
-
+  <view :class="classes" ref="scroller">
     <view class="nut-infinite-container">
       <slot></slot>
     </view>
@@ -14,16 +7,16 @@
     <view class="nut-infinite-bottom">
       <template v-if="isInfiniting">
         <view class="bottom-box">
-          <template v-if="!slots.loading">
-            <nut-icon class="bottom-img" v-bind="$attrs" :name="loadIcon"></nut-icon>
+          <slot name="loading">
+            <nut-icon v-if="loadIcon" class="bottom-img" v-bind="$attrs" :name="loadIcon"></nut-icon>
             <view class="bottom-text">{{ loadTxt || translate('loading') }}</view>
-          </template>
-          <slot name="loading" v-else></slot>
+          </slot>
         </view>
       </template>
       <template v-else-if="!hasMore">
-        <view class="tips" v-if="!slots.finished">{{ loadMoreTxt || translate('loadMoreTxt') }}</view>
-        <slot name="finished" v-else></slot>
+        <slot name="finished">
+          <view class="tips">{{ loadMoreTxt || translate('loadMoreTxt') }}</view>
+        </slot>
       </template>
     </view>
   </view>
@@ -35,17 +28,24 @@ import {
   onUnmounted,
   reactive,
   computed,
-  CSSProperties,
   onActivated,
   onDeactivated,
-  ref
+  ref,
+  watch,
+  nextTick
 } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { componentName, create, translate } = createComponent('infiniteloading');
 import { useTouch } from '@/packages/utils/useTouch';
+import requestAniFrame from '@/packages/utils/raf';
+import { getScrollTopRoot } from '@/packages/utils/util';
 
 export default create({
   props: {
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
     hasMore: {
       type: Boolean,
       default: true
@@ -54,17 +54,9 @@ export default create({
       type: Number,
       default: 200
     },
-    pullIcon: {
-      type: String,
-      default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
-    },
-    pullTxt: {
-      type: String,
-      default: ''
-    },
     loadIcon: {
       type: String,
-      default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
+      default: ''
     },
     loadTxt: {
       type: String,
@@ -85,24 +77,17 @@ export default create({
     useCapture: {
       type: Boolean,
       default: false
-    },
-    isOpenRefresh: {
-      type: Boolean,
-      default: false
     }
   },
-  emits: ['scroll-change', 'load-more', 'refresh'],
+  emits: ['scroll-change', 'load-more', 'update:modelValue'],
 
   setup(props, { emit, slots }) {
     const touch: any = useTouch();
     const state = reactive({
       scrollEl: window as Window | HTMLElement | (Node & ParentNode),
       scroller: null as null | HTMLElement,
-      refreshTop: null as null | HTMLElement,
       beforeScrollTop: 0,
-      isTouching: false,
       isInfiniting: false,
-      refreshMaxH: 0,
       y: 0,
       x: 0,
       distance: 0
@@ -115,32 +100,6 @@ export default create({
       };
     });
 
-    const getStyle = computed(() => {
-      const style: CSSProperties = {};
-      return {
-        height: state.distance < 0 ? `0px` : `${state.distance}px`,
-        transition: state.isTouching
-          ? `height 0s cubic-bezier(0.25,0.1,0.25,1)`
-          : `height 0.2s cubic-bezier(0.25,0.1,0.25,1)`
-      };
-    });
-
-    const requestAniFrame = () => {
-      return (
-        window.requestAnimationFrame ||
-        window.webkitRequestAnimationFrame ||
-        function (callback) {
-          window.setTimeout(callback, 1000 / 60);
-        }
-      );
-    };
-
-    const getWindowScrollTop = () => {
-      return window.pageYOffset !== undefined
-        ? window.pageYOffset
-        : (document.documentElement || document.body.parentNode || document.body).scrollTop;
-    };
-
     const calculateTopPosition = (el: HTMLElement): number => {
       return !el ? 0 : el.offsetTop + calculateTopPosition(el.offsetParent as HTMLElement);
     };
@@ -149,8 +108,10 @@ export default create({
       let offsetDistance = 0;
       let resScrollTop = 0;
       let direction = 'down';
-      const windowScrollTop = getWindowScrollTop();
+
       if (props.useWindow) {
+        const windowScrollTop = getScrollTopRoot();
+
         if (state.scroller) {
           offsetDistance =
             calculateTopPosition(state.scroller) + state.scroller.offsetHeight - windowScrollTop - window.innerHeight;
@@ -176,17 +137,15 @@ export default create({
       return offsetDistance <= props.threshold && direction == 'down';
     };
 
-    const infiniteDone = () => {
-      state.isInfiniting = false;
-    };
-
     const handleScroll = () => {
-      requestAniFrame()(() => {
+      requestAniFrame(() => {
         if (!isScrollAtBottom() || !props.hasMore || state.isInfiniting) {
           return false;
         } else {
           state.isInfiniting = true;
-          emit('load-more', infiniteDone);
+
+          emit('update:modelValue', true);
+          nextTick(() => emit('load-more'));
         }
       });
     };
@@ -195,67 +154,24 @@ export default create({
       state.scrollEl.addEventListener('scroll', handleScroll, props.useCapture);
     };
 
-    const refreshDone = () => {
-      state.distance = 0;
-      state.isTouching = false;
-    };
-
-    const touchStart = (event: TouchEvent) => {
-      touch.start(event);
-
-      if (state.beforeScrollTop == 0 && !state.isTouching && props.isOpenRefresh) {
-        state.y = event.touches[0].pageY;
-        state.isTouching = true;
-
-        const childHeight = ((state.refreshTop as HTMLElement).firstElementChild as HTMLElement).offsetHeight;
-        state.refreshMaxH = Math.floor(childHeight * 1 + 10);
-      }
-    };
-
-    const touchMove = (event: TouchEvent) => {
-      touch.move(event);
-
-      state.distance = event.touches[0].pageY - state.y;
-
-      if ((touch as any).isVertical() && state.distance > 0 && state.isTouching) {
-        event.preventDefault();
-        if (state.distance >= state.refreshMaxH) state.distance = state.refreshMaxH;
-      } else {
-        state.distance = 0;
-        state.isTouching = false;
-      }
-    };
-
-    const touchEnd = () => {
-      if (state.distance) {
-        if (state.distance < state.refreshMaxH) {
-          state.distance = 0;
-        } else {
-          emit('refresh', refreshDone);
-        }
-      }
-
-      setTimeout(() => {
-        touch.reset();
-      }, 0);
-    };
-
     // 滚动监听对象
     const getParentElement = (el: HTMLElement) => {
       return !!props.containerId ? document.querySelector(`#${props.containerId}`) : el && el.parentNode;
     };
 
+    const removeScrollListener = () => {
+      state.scrollEl.removeEventListener('scroll', handleScroll, props.useCapture);
+    };
+
     onMounted(() => {
       const parentElement = getParentElement(state.scroller as HTMLElement) as Node & ParentNode;
       state.scrollEl = props.useWindow ? window : parentElement;
 
       scrollListener();
-
-      console.log(slots);
     });
 
     onUnmounted(() => {
-      state.scrollEl.removeEventListener('scroll', handleScroll, props.useCapture);
+      removeScrollListener();
     });
 
     const isKeepAlive = ref(false);
@@ -269,16 +185,23 @@ export default create({
 
     onDeactivated(() => {
       isKeepAlive.value = true;
-      state.scrollEl.removeEventListener('scroll', handleScroll, props.useCapture);
+      removeScrollListener();
     });
 
+    watch(
+      () => props.modelValue,
+      (val) => {
+        console.log('监听', val);
+        if (val) {
+        } else {
+          state.isInfiniting = false;
+        }
+      }
+    );
+
     return {
       classes,
       ...toRefs(state),
-      touchStart,
-      touchMove,
-      touchEnd,
-      getStyle,
       translate,
       slots
     };

+ 54 - 0
src/packages/__VUE/pullrefresh/__tests__/__snapshots__/infiniteloading.spec.ts.snap

@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`infiniteloading base 1`] = `
+"<view class=\\"nut-infiniteloading\\">
+  <view class=\\"nut-infinite-top\\" style=\\"height: 0px; transition: height 0.2s cubic-bezier(0.25,0.1,0.25,1);\\">
+    <view class=\\"top-box\\"><img class=\\"nut-icon__img top-img\\" src=\\"https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png\\">
+      <view class=\\"top-text\\">松开刷新</view>
+    </view>
+  </view>
+  <view class=\\"nut-infinite-container\\">
+    <li class=\\"infiniteLi\\">0</li>
+    <li class=\\"infiniteLi\\">1</li>
+    <li class=\\"infiniteLi\\">2</li>
+    <li class=\\"infiniteLi\\">3</li>
+    <li class=\\"infiniteLi\\">4</li>
+    <li class=\\"infiniteLi\\">5</li>
+    <li class=\\"infiniteLi\\">6</li>
+    <li class=\\"infiniteLi\\">7</li>
+    <li class=\\"infiniteLi\\">8</li>
+    <li class=\\"infiniteLi\\">9</li>
+  </view>
+  <view class=\\"nut-infinite-bottom\\">
+    <!--v-if-->
+  </view>
+</view>"
+`;
+
+exports[`pull base 1`] = `
+"<view class=\\"nut-infiniteloading\\">
+  <view class=\\"nut-infinite-top\\" style=\\"height: 0px; transition: height 0.2s cubic-bezier(0.25,0.1,0.25,1);\\">
+    <view class=\\"top-box\\"><img class=\\"nut-icon__img top-img\\" src=\\"https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png\\">
+      <view class=\\"top-text\\">松开刷新</view>
+    </view>
+  </view>
+  <view class=\\"nut-infinite-container\\"></view>
+  <view class=\\"nut-infinite-bottom\\">
+    <!--v-if-->
+  </view>
+</view>"
+`;
+
+exports[`pull base 2`] = `
+"<view class=\\"nut-infiniteloading\\">
+  <view class=\\"nut-infinite-top\\" style=\\"height: 0px; transition: height 0.2s cubic-bezier(0.25,0.1,0.25,1);\\">
+    <view class=\\"top-box\\"><img class=\\"nut-icon__img top-img\\" src=\\"https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png\\">
+      <view class=\\"top-text\\">松开刷新</view>
+    </view>
+  </view>
+  <view class=\\"nut-infinite-container\\"></view>
+  <view class=\\"nut-infinite-bottom\\">
+    <!--v-if-->
+  </view>
+</view>"
+`;

+ 96 - 0
src/packages/__VUE/pullrefresh/__tests__/infiniteloading.spec.ts

@@ -0,0 +1,96 @@
+import { config, mount } from '@vue/test-utils';
+import InfiniteLoading from '../index.vue';
+import NutIcon from '../../icon/index.vue';
+import { trigger, triggerDrag } from '../../../utils/test/event';
+import { nextTick, toRefs, reactive, ref, onMounted } from 'vue';
+
+function sleep(delay = 0): Promise<void> {
+  return new Promise((resolve) => {
+    setTimeout(resolve, delay);
+  });
+}
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('pull base', async () => {
+  const wrapper = mount(InfiniteLoading, {
+    props: {
+      isOpenRefresh: true,
+      loadMoreTxt: '没有更多'
+    }
+  });
+  const track = wrapper.find('.nut-infiniteloading');
+  // pulling
+  trigger(track, 'touchstart', 0, 0);
+  trigger(track, 'touchmove', 0, 20);
+  expect(wrapper.html()).toMatchSnapshot();
+
+  // loading
+  trigger(track, 'touchend', 0, 100);
+  expect(wrapper.html()).toMatchSnapshot();
+
+  // still loading
+  triggerDrag(track, 0, 100);
+
+  expect(wrapper.emitted('refresh')).toBeTruthy();
+});
+
+test('infiniteloading base', async () => {
+  const wrapper = mount({
+    components: {
+      'nut-infiniteloading': InfiniteLoading,
+      'nut-icon': NutIcon
+    },
+    template: `
+    <nut-infiniteloading
+      :has-more="hasMore"
+      load-more-txt="没有啦~"
+      @load-more="loadMore">
+        <li class="infiniteLi" v-for="(item, index) in defultList" :key="index">{{item}}</li>
+    </nut-infiniteloading>
+    `,
+    setup() {
+      const hasMore = ref(true);
+      const data = reactive({
+        defultList: []
+      });
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          (data.defultList as any).push(i);
+        }
+      };
+      const loadMore = (done: any) => {
+        setTimeout(() => {
+          const curLen = data.defultList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            (data.defultList as any).push(i);
+          }
+          if (data.defultList.length > 30) hasMore.value = false;
+          done();
+        }, 500);
+      };
+      onMounted(() => {
+        init();
+      });
+
+      return { ...toRefs(data), hasMore, loadMore };
+    }
+  });
+  const track = wrapper.find('.nut-infiniteloading');
+  await nextTick();
+  trigger(track, 'touchstart', 0, 0);
+  trigger(track, 'touchmove', 0, -100);
+  trigger(track, 'touchend', 0, -800);
+
+  triggerDrag(track, 0, -800);
+  await nextTick();
+  expect(wrapper.html()).toMatchSnapshot();
+});

+ 116 - 0
src/packages/__VUE/pullrefresh/demo.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="demo">
+    <nut-tabs v-model="tabsValue" animatedTime="0">
+      <nut-tabpane title="基础用法">
+        <nut-pullrefresh v-model="refresh" @refresh="refreshFun">
+          <div class="pull-block">向下拉试试吧!</div>
+        </nut-pullrefresh>
+      </nut-tabpane>
+      <nut-tabpane title="自定义文案">
+        <nut-pullrefresh
+          v-model="refresh2"
+          pullingTxt="用力拉"
+          loosingTxt="松开吧"
+          loadingTxt="玩命加载中..."
+          completeTxt="好啦"
+          @refresh="refreshFun"
+        >
+          <div class="pull-block">向下拉试试吧!</div>
+        </nut-pullrefresh>
+      </nut-tabpane>
+      <nut-tabpane title="Tab 3">
+        <div class="parentpage">
+          <nut-pullrefresh v-model="refresh" @refresh="refreshFun">
+            <div class="pull-block" v-for="item in refreshList">
+              <div>item</div>
+            </div>
+          </nut-pullrefresh>
+        </div>
+      </nut-tabpane>
+    </nut-tabs>
+  </div>
+</template>
+
+<script lang="ts">
+import { onMounted, ref, reactive, toRefs, getCurrentInstance } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+
+const { createDemo, translate } = createComponent('pullrefresh');
+import { useTranslate } from '@/sites/assets/util/useTranslate';
+
+const initTranslate = () =>
+  useTranslate({
+    'zh-CN': {
+      basic: '基础用法',
+      pullRefresh: '下拉刷新',
+      customTxt: '自定义加载文案',
+      none: '没有啦~',
+      success: '刷新成功'
+    },
+    'en-US': {
+      basic: 'Basic Usage',
+      pullRefresh: 'Pull to refresh',
+      customTxt: 'Custom loading copywriting',
+      none: 'No more',
+      success: 'Refresh success'
+    }
+  });
+
+export default createDemo({
+  props: {},
+  setup() {
+    initTranslate();
+
+    const refresh = ref(false);
+    const refresh2 = ref(false);
+
+    const data = reactive({
+      tabsValue: 0,
+      customList: [],
+      refreshList: new Array(10).fill(11)
+    });
+
+    const refreshFun = () => {
+      setTimeout(() => {
+        console.log('加载中');
+        refresh.value = false;
+        refresh2.value = false;
+      }, 3000);
+    };
+
+    return {
+      refreshFun,
+      refresh,
+      refresh2,
+      translate,
+      ...toRefs(data)
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+.demo {
+  padding-left: 0 !important;
+  padding-right: 0 !important;
+  padding-bottom: 0 !important;
+}
+
+.nut-tabpane {
+  padding: 0;
+}
+.nut-pullrefresh {
+  height: calc(100vh - 107px);
+}
+.pull-block {
+  text-align: center;
+  color: #999;
+  font-size: 16px;
+  padding-top: 60px;
+}
+
+.parentpage {
+  height: 600px;
+  /* background: #ee; */
+}
+</style>

+ 278 - 0
src/packages/__VUE/pullrefresh/doc.en-US.md

@@ -0,0 +1,278 @@
+# InfiniteLoading
+
+### Intro
+
+Scrolling to the bottom of the list automatically loads more data.
+
+### Install
+
+```javascript
+  import { createApp } from 'vue';
+  import { InfiniteLoading } from '@nutui/nutui';
+
+  const app = createApp();
+  app.use(InfiniteLoading);
+```
+
+### Basic Usage
+
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="scroll"  style='height: 300px;'>
+    <nut-infiniteloading
+        containerId = 'scroll'
+        :use-window='false'
+        :has-more="hasMore"
+        @load-more="loadMore"
+    >
+        <li class="infiniteLi" v-for="(item, index) in defultList" :key="index">{{item}}</li>
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  export default {
+    setup(props) {
+      const hasMore = ref(true);
+      const data = reactive({
+        defultList: []
+      });
+      const loadMore = done => {  
+        setTimeout(() => {
+          const curLen = data.defultList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.defultList.push(`${i}`);
+          }
+          if (data.defultList.length > 30) hasMore.value = false;
+          done()
+        }, 500);
+      };
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.defultList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { loadMore, hasMore, ...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+
+:::
+
+### Pull to refresh
+
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="refreshScroll" style='height: 300px;'>
+    <nut-infiniteloading
+      pull-icon="JD"
+      container-id="refreshScroll"
+      :use-window="false"
+      :is-open-refresh="true"
+      :has-more="refreshHasMore"
+      @load-more="refreshLoadMore"
+      @refresh="refresh"
+    >
+      <li
+        class="infiniteLi"
+        v-for="(item, index) in refreshList"
+        :key="index"
+        >{{ item }}</li
+      >
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    setup(props) {
+      const refreshHasMore = ref(true);
+      const data = reactive({
+        refreshList: []
+      });
+      const refreshLoadMore = done => {
+        setTimeout(() => {
+          const curLen = data.refreshList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.refreshList.push(
+              `${i}`
+            );
+          }
+          if (data.refreshList.length > 30) refreshHasMore.value = false;
+          done()
+        }, 500);
+      };
+
+      const refresh = (done) => {
+        setTimeout(()=>{
+          Toast.success('Refresh success');
+          done()
+        },1000)
+      }
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.refreshList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { refreshLoadMore, refreshHasMore, refresh, ...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+
+:::
+
+### Custom loading copywriting
+
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="customScroll">
+    <nut-infiniteloading
+      load-txt="loading"
+      load-more-txt="No more"
+      container-id="customScroll"
+      :use-window="false"
+      :has-more="customHasMore"
+      @load-more="customLoadMore"
+    >
+      <li class="infiniteLi" v-for="(item, index) in customList" :key="index">{{ item }}</li>
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  export default {
+    setup(props) {
+      const customHasMore = ref(true);
+      const data = reactive({
+        customList: ['']
+      });
+      const customLoadMore = done => {
+        setTimeout(() => {
+          const curLen = data.customList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.customList.push(`${i}`);
+          }
+          if (data.customList.length > 30) customHasMore.value = false;
+          done()
+        }, 500);
+      };
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.customList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { customHasMore, customLoadMore,...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+
+:::
+
+## API
+
+### Props
+
+| Attribute        | Description                                                                                                        | Type    | Default          |
+|------------------|--------------------------------------------------------------------------------------------------------------------|---------|------------------|
+| has-more         | Has more data                                                                                                      | Boolean | `true`           |
+| threshold        | The loadMore event will be Emitted when the distance between the scrollbar and the bottom is less than threshold   | Number  | `200`            |
+| use-window       | Add the scroll listener to the window or the parent of the listening component                                     | Boolean | `true`           |
+| use-capture      | Whether to use capture mode                                                                                        | Boolean | `false`          |
+| container-id     | When useWindow is false, set the node ID by default                                                                | String  | `''`             |
+| load-more-txt    | "No more" text                                                                                                     | String  | 'Oops, this is the bottom'|
+| is-open-refresh  | Enable pull refresh                                                                                                | Boolean | `false`          |
+| pull-icon        | Pull refresh[图标名称](#/zh-CN/component/icon)                                                                                       | String  | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40/>                |
+| pull-txt         | Pull refresh text                                                                                                   | String  |`Loose to refresh`|
+| load-icon        | Pull on loading[图标名称](#/zh-CN/component/icon)                                                                                    | String | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40 />                |
+| load-txt         | Pull on loading text                                                                                                | String  | `Loading...`      |
+
+### Events
+
+| Event          | Description                         | Arguments      |
+|----------------|-------------------------------------|----------------|
+| load-more      | Emitted when continues to load      | done()         |
+| scroll-change  | Real-time monitoring of roll height | height         |
+| refresh        | Emitted when pull refresh           | done()         |
+
+### Slots
+
+| Attribute | Description  | 
+|--------|----------------|
+| loading  | Loading text |
+| finished  | Finished text |

+ 273 - 0
src/packages/__VUE/pullrefresh/doc.md

@@ -0,0 +1,273 @@
+#  InfiniteLoading 滚动加载
+
+### 介绍
+
+列表滚动到底部自动加载更多数据。
+
+### 安装
+
+```javascript
+  import { createApp } from 'vue';
+  import { InfiniteLoading } from '@nutui/nutui';
+
+  const app = createApp();
+  app.use(InfiniteLoading);
+```
+
+### 基础用法
+
+
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="scroll"  style='height: 300px;'>
+    <nut-infiniteloading
+        containerId = 'scroll'
+        :use-window='false'
+        :has-more="hasMore"
+        @load-more="loadMore"
+    >
+        <li class="infiniteLi" v-for="(item, index) in defultList" :key="index">{{item}}</li>
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  export default {
+    setup(props) {
+      const hasMore = ref(true);
+      const data = reactive({
+        defultList: []
+      });
+      const loadMore = done => {  
+        setTimeout(() => {
+          const curLen = data.defultList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.defultList.push(`${i}`);
+          }
+          if (data.defultList.length > 30) hasMore.value = false;
+          done()
+        }, 500);
+      };
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.defultList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { loadMore, hasMore, ...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+:::
+### 下拉刷新
+
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="refreshScroll" style='height: 300px;'>
+    <nut-infiniteloading
+      pull-icon="JD"
+      container-id="refreshScroll"
+      :use-window="false"
+      :is-open-refresh="true"
+      :has-more="refreshHasMore"
+      @load-more="refreshLoadMore"
+      @refresh="refresh"
+    >
+      <li
+        class="infiniteLi"
+        v-for="(item, index) in refreshList"
+        :key="index"
+        >{{ item }}</li
+      >
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    setup(props) {
+      const refreshHasMore = ref(true);
+      const data = reactive({
+        refreshList: []
+      });
+      const refreshLoadMore = done => {
+        setTimeout(() => {
+          const curLen = data.refreshList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.refreshList.push(
+              `${i}`
+            );
+          }
+          if (data.refreshList.length > 30) refreshHasMore.value = false;
+          done()
+        }, 500);
+      };
+
+      const refresh = (done) => {
+        setTimeout(()=>{
+          Toast.success('刷新成功');
+          done()
+        },1000)
+      }
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.refreshList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { refreshLoadMore, refreshHasMore, refresh, ...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+:::
+### 自定义加载文案
+:::demo
+
+```html
+<template>
+  <ul class="infiniteUl" id="customScroll">
+    <nut-infiniteloading
+      load-txt="loading"
+      load-more-txt="没有啦~"
+      container-id="customScroll"
+      :use-window="false"
+      :has-more="customHasMore"
+      @load-more="customLoadMore"
+    >
+      <li class="infiniteLi" v-for="(item, index) in customList" :key="index">{{ item }}</li>
+    </nut-infiniteloading>
+  </ul>
+</template>
+
+<script>
+  import { ref,reactive,onMounted,toRefs} from 'vue';
+  export default {
+    setup(props) {
+      const customHasMore = ref(true);
+      const data = reactive({
+        customList: ['']
+      });
+      const customLoadMore = done => {
+        setTimeout(() => {
+          const curLen = data.customList.length;
+          for (let i = curLen; i < curLen + 10; i++) {
+            data.customList.push(`${i}`);
+          }
+          if (data.customList.length > 30) customHasMore.value = false;
+          done()
+        }, 500);
+      };
+      const init = () => {
+        for (let i = 0; i < 10; i++) {
+          data.customList.push(`${i}`);
+        }
+      }
+      onMounted(() => {
+        init()
+      });
+      return { customHasMore, customLoadMore,...toRefs(data) };
+    }
+  }
+</script>
+
+<style>
+.infiniteUl {
+  height: 300px;
+  width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background:#eee
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+</style>
+```
+:::
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| has-more         | 是否还有更多数据               | Boolean | `true`                |
+| threshold         | 滚动条与底部距离小于 threshold 时触发 loadMore 事件 | Number | `200`               |
+| use-window | 将滚动侦听器添加到 window 否则侦听组件的父节点     | Boolean | `true` |
+| use-capture          | 是否使用捕获模式 true 捕获 false 冒泡                        | Boolean | `false`            |
+| container-id          | 在 useWindow 属性为 false 的时候,自定义设置节点ID                        | String | `''`            |
+| load-more-txt          | “没有更多数”据展示文案                        | String | `'哎呀,这里是底部了啦'`            |
+| is-open-refresh        | 是否开启下拉刷新                         | Boolean | `false`                |
+| pull-icon        | 下拉刷新[图标名称](#/zh-CN/component/icon)                        | String | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40/>                |
+| pull-txt        | 下拉刷新提示文案                         | String | `松手刷新`                |
+| load-icon        | 上拉加载[图标名称](#/zh-CN/component/icon)                       | String | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40 />                |
+| load-txt        | 上拉加载提示文案                         | String | `加载中...`                |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| load-more  | 继续加载的回调函数 | done 函数,用于关闭加载中状态 |
+| scroll-change  | 实时监听滚动高度 | 滚动高度 |
+| refresh  | 下拉刷新事件回调 | done 函数,用于关闭加载中状态 |
+
+### Slots
+
+| 名称 | 说明           | 
+|--------|----------------|
+| loading  | 自定义底部记载中提示 |
+| finished  | 自定义加载完成后的提示文案 |

+ 115 - 0
src/packages/__VUE/pullrefresh/doc.taro.md

@@ -0,0 +1,115 @@
+#  InfiniteLoading 滚动加载
+
+### 介绍
+
+列表滚动到底部自动加载更多数据。
+
+#### 直接使用 Taro 现有 ScrollView 组件开发 [参考文档](https://docs.taro.zone/docs/components/viewContainer/scroll-view)
+
+<!-- ### 安装
+
+```javascript
+  import { createApp } from 'vue';
+  import { InfiniteLoading } from '@nutui/nutui-taro';
+
+  const app = createApp();
+  app.use(InfiniteLoading);
+```
+
+## 代码演示
+    
+### 基础用法
+
+在一个页面中,只能有一个 Infiniteloading,不可同时存在两个以及更多。
+
+```html
+<div class="infiniteUl" id="scrollDemo">
+  <nut-infiniteloading
+    pull-icon="JD"
+    load-txt="loading"
+    load-more-txt="没有啦~"
+    :is-open-refresh="true"
+    container-id="scrollDemo"
+    :has-more="hasMore"
+    @load-more="loadMore"
+    @refresh="refresh"
+  >
+    <div
+      class="infiniteLi"
+      v-for="(item, index) in defultList"
+      :key="index"
+      >{{ item }}</div
+    >
+  </nut-infiniteloading>
+</div>
+```
+```javascript
+setup() {
+    const hasMore = ref(true);
+
+    const data = reactive({
+      defultList: ['']
+    });
+
+    const loadMore = (done) => {
+      setTimeout(() => {
+        const curLen = data.defultList.length;
+
+        for (let i = curLen; i < curLen + 10; i++) {
+          data.defultList.push(`${i}`);
+        }
+
+        if (data.defultList.length > 30) hasMore.value = false;
+
+        done();
+      }, 500);
+    };
+
+    const refresh = (done) => {
+      setTimeout(() => {
+        console.log('刷新成功');
+        done();
+      }, 1000);
+    };
+
+    const init = () => {
+      for (let i = 0; i < 20; i++) {
+        data.defultList.push(`${i}`);
+      }
+    };
+    onMounted(() => {
+      init();
+    });
+    return {
+      loadMore,
+      hasMore,
+      refresh,
+      ...toRefs(data)
+    };
+}
+```
+
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| has-more         | 是否还有更多数据               | Boolean | `true`                |
+| container-id          | 必填,设置滚动节点ID                        | String | `''`            |
+| load-more-txt          | “没有更多数”据展示文案                        | String | `'哎呀,这里是底部了啦'`            |
+| is-open-refresh        | 是否开启下拉刷新                         | Boolean | `false`                |
+| pull-icon        | 下拉刷新[图标名称](#/zh-CN/component/icon)                        | String | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40/>                |
+| pull-txt        | 下拉刷新提示文案                         | String | `松手刷新`                |
+| load-icon        | 上拉加载[图标名称](#/zh-CN/component/icon)                       | Boolean | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40 />                |
+| load-txt        | 上拉加载提示文案                         | String | `加载中...`                |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| load-more  | 继续加载的回调函数 | done 函数,用于关闭加载中状态 |
+| scroll-change  | 实时监听滚动高度 | 滚动高度 |
+| refresh  | 下拉刷新事件回调 | done 函数,用于关闭加载中状态 | -->
+    

+ 32 - 0
src/packages/__VUE/pullrefresh/index.scss

@@ -0,0 +1,32 @@
+.nut-pullrefresh {
+  height: 100%;
+  overflow: hidden;
+  &-container {
+    position: relative;
+    height: 100%;
+
+    &-topbox {
+      position: absolute;
+      left: 0;
+      width: 100%;
+      height: 50px;
+      transform: translateY(-100%);
+      text-align: center;
+      line-height: 50px;
+      font-size: 14px;
+      // display: flex;
+      // flex-direction: column;
+      // align-items: center;
+      // justify-content: center;
+
+      &-icon {
+        width: 28px;
+        height: 24px;
+      }
+      &-text {
+        font-size: 14px;
+        color: $text-color;
+      }
+    }
+  }
+}

+ 226 - 0
src/packages/__VUE/pullrefresh/index.vue

@@ -0,0 +1,226 @@
+<template>
+  <div :class="classes" ref="scroller" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
+    <div class="nut-pullrefresh-container" ref="refreshTop" :style="getStyle">
+      <div class="nut-pullrefresh-container-topbox" :style="getHeightStyle">
+        <!-- <nut-icon class="nut-pullrefresh-container-topbox-icon" v-bind="$attrs" :name="pullIcon"></nut-icon> -->
+        <div class="nut-pullrefresh-container-topbox-text" v-if="status == 'pulling'">{{ pullingTxt }}</div>
+
+        <div class="nut-pullrefresh-container-topbox-text" v-if="status == 'loosing'">{{ loosingTxt }}</div>
+
+        <div class="nut-pullrefresh-container-topbox-text" v-if="status == 'loading'">{{ loadingTxt }}</div>
+
+        <div class="nut-pullrefresh-container-topbox-text" v-if="status == 'complete'">{{ completeTxt }}</div>
+      </div>
+
+      <slot></slot>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+import { toRefs, reactive, computed, CSSProperties, onActivated, onDeactivated, ref, nextTick, watch } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { componentName, create, translate } = createComponent('pullrefresh');
+import { useTouch } from '@/packages/utils/useTouch';
+import { getScrollTopRoot } from '@/packages/utils/util';
+import { pxCheck } from '@/packages/utils/pxCheck';
+
+type PullRefreshStatus = 'normal' | 'loading' | 'loosing' | 'pulling' | 'complete';
+
+export default create({
+  props: {
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    pullIcon: {
+      type: String,
+      default: ''
+    },
+    pullingTxt: {
+      type: String,
+      default: '下拉刷新'
+    },
+    loosingTxt: {
+      type: String,
+      default: '释放刷新'
+    },
+    loadingTxt: {
+      type: String,
+      default: '加载中...'
+    },
+
+    completeTxt: {
+      type: String,
+      default: ''
+    },
+    headHeight: {
+      type: [String, Number],
+      default: 50
+    },
+
+    pullDistance: {
+      type: [String, Number],
+      default: 50
+    },
+
+    duration: {
+      type: [String, Number],
+      default: 0.3
+    },
+
+    useWindow: {
+      type: Boolean,
+      default: true
+    },
+    containerId: {
+      type: String,
+      default: ''
+    },
+    useCapture: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['change', 'refresh', 'update:modelValue'],
+
+  setup(props, { emit, slots }) {
+    const touch: any = useTouch();
+    const state = reactive({
+      scroller: null as null | HTMLElement,
+      refreshTop: null as null | HTMLElement,
+      isPullRefresh: false,
+      distance: 0,
+      status: 'normal' // 当前状态
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const getStyle = computed(() => {
+      return {
+        transitionDuration: `${props.duration}s`,
+        transform: state.distance ? `translate3d(0,${state.distance}px, 0)` : ''
+      };
+    });
+
+    const getHeightStyle = computed(() => {
+      const styles: CSSProperties = {};
+      if (props.headHeight != 50) styles.height = pxCheck(props.headHeight);
+      return styles;
+    });
+
+    const timing = (distance: number) => {
+      const pullDistance = +(props.pullDistance || props.headHeight);
+      let moveDistance = distance;
+      if (distance > pullDistance) {
+        if (distance < pullDistance * 2) {
+          moveDistance = pullDistance + (distance - pullDistance) / 2;
+        } else {
+          moveDistance = pullDistance + distance / 4;
+        }
+      }
+
+      return Math.round(moveDistance);
+    };
+
+    const setPullStatus = (distance: number, isLoading?: boolean) => {
+      const pullDistance = +(props.pullDistance || props.headHeight);
+      state.distance = distance;
+
+      if (isLoading) {
+        state.status = 'loading';
+      } else if (distance === 0) {
+        state.status = 'normal';
+      } else if (distance < pullDistance) {
+        state.status = 'pulling';
+      } else {
+        state.status = 'loosing';
+      }
+
+      emit('change', { status: state.status, distance });
+    };
+
+    const isCanTouch = () => state.status !== 'loading' && state.status !== 'complete';
+
+    const touchStart = (event: TouchEvent) => {
+      if (isCanTouch()) {
+        if (getScrollTopRoot() == 0) {
+          touch.start(event);
+          state.isPullRefresh = true;
+        } else {
+          state.distance = 0;
+          state.isPullRefresh = false;
+        }
+      }
+    };
+
+    const touchMove = (event: TouchEvent) => {
+      if (isCanTouch()) {
+        touch.move(event);
+
+        const { deltaY } = touch;
+
+        if ((touch as any).isVertical() && deltaY.value > 0 && state.isPullRefresh) {
+          event.preventDefault();
+          setPullStatus(timing(deltaY.value));
+        }
+      }
+    };
+
+    const touchEnd = () => {
+      if (state.isPullRefresh && isCanTouch() && touch.deltaY.value) {
+        if (state.status === 'loosing') {
+          setPullStatus(+props.headHeight, true);
+          emit('update:modelValue', true);
+          nextTick(() => emit('refresh'));
+        } else {
+          setPullStatus(0);
+        }
+      }
+
+      setTimeout(() => {
+        touch.reset();
+      }, 0);
+    };
+
+    const isKeepAlive = ref(false);
+
+    onActivated(() => {
+      if (isKeepAlive.value) {
+        isKeepAlive.value = false;
+      }
+    });
+
+    onDeactivated(() => {
+      isKeepAlive.value = true;
+    });
+
+    watch(
+      () => props.modelValue,
+      (val) => {
+        if (val) {
+          setPullStatus(+props.headHeight, true);
+        } else {
+          setPullStatus(0);
+        }
+      }
+    );
+
+    return {
+      classes,
+      ...toRefs(state),
+      touchStart,
+      touchMove,
+      touchEnd,
+      getStyle,
+      translate,
+      slots,
+      getHeightStyle
+    };
+  }
+});
+</script>

+ 14 - 0
src/packages/utils/util.ts

@@ -130,3 +130,17 @@ export function preventDefault(event: Event, isStopPropagation?: boolean) {
     event.stopPropagation();
   }
 }
+
+export const padZero = (num: number | string, length = 2): string => {
+  num += '';
+  while ((num as string).length < length) {
+    num = '0' + num;
+  }
+  return num.toString();
+};
+
+export const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max);
+
+export const getScrollTopRoot = (): number => {
+  return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+};