u-pull-refresh.vue 7.55 KB
<template>
  <view 
    class="u-pull-refresh"
    @touchstart="onTouchStart"
    @touchmove="onTouchMove"
    @touchend="onTouchEnd"
    @touchcancel="onTouchEnd"
  >
    <!-- 下拉刷新区域 -->
    <view 
      class="refresh-area" 
      :style="{ height: refreshDistance + 'px' }"
      :class="{ refreshing: isRefreshing }"
    >
      <!-- 不同状态的插槽 -->
      <slot 
        v-if="refreshStatus === 'pull'" 
        name="pull" 
        :distance="refreshDistance"
        :threshold="threshold"
      >
        <!-- 默认下拉状态 -->
        <view class="refresh-content">
          <view class="refresh-indicator">
            <up-icon name="arrow-downward" size="26px"></up-icon>
          </view>
          <text class="refresh-text">{{ t("up.pullRefresh.pull") }}</text>
        </view>
      </slot>
      
      <slot 
        v-else-if="refreshStatus === 'release'" 
        name="release" 
        :distance="refreshDistance"
        :threshold="threshold"
      >
        <!-- 默认释放状态 -->
        <view class="refresh-content">
          <view class="refresh-indicator">
            <up-icon name="arrow-upward" size="26px"></up-icon>
          </view>
          <text class="refresh-text">{{ t("up.pullRefresh.release") }}</text>
        </view>
      </slot>
      
      <slot 
        v-else-if="refreshStatus === 'refreshing'" 
        name="refreshing"
      >
        <!-- 默认刷新中状态 -->
        <view class="refresh-content">
          <view class="refresh-indicator">
            <view class="spinner"></view>
          </view>
          <text class="refresh-text">{{ t("up.pullRefresh.refreshing") }}...</text>
        </view>
      </slot>
    </view>
    
    <!-- 内容区域 -->
    <view 
      class="refresh-content-wrapper"
      :style="{ transform: `translateY(${contentTranslateY}px)` }"
    >
      <scroll-view
        v-if="useScrollView"
        class="scroll-wrapper"
        :scroll-y="true"
        :enable-back-to-top="enableBackToTop"
        :scroll-top="scrollTop"
        :lower-threshold="lowerThreshold"
        @scroll="handleScroll"
        @scrolltolower="handleScrollToLower"
      >
        <slot></slot>
        
        <!-- 使用 u-loadmore 组件实现上拉加载更多 -->
        <u-loadmore
          v-if="showLoadmore"
          v-bind="loadmoreProps"
        />
      </scroll-view>
      
      <view v-else>
        <slot></slot>
        
        <!-- 使用 u-loadmore 组件实现上拉加载更多 -->
        <u-loadmore
          v-if="showLoadmore"
          v-bind="loadmoreProps"
        />
      </view>
    </view>
  </view>
</template>

<script>
import { t } from '../../libs/i18n'
export default {
  name: 'u-pull-refresh',
  props: {
    // 是否正在刷新
    refreshing: {
      type: Boolean,
      default: false
    },
    // 下拉刷新阈值
    threshold: {
      type: Number,
      default: 80
    },
    // 阻尼系数
    damping: {
      type: Number,
      default: 0.4
    },
    // 最大下拉距离
    maxDistance: {
      type: Number,
      default: 120
    },
    // 是否显示加载更多
    showLoadmore: {
      type: Boolean,
      default: false
    },
    // u-loadmore 组件的 props 配置
    loadmoreProps: {
      type: Object,
      default: () => ({
        status: 'loadmore',
        // loadmoreText: '加载更多',
        // loadingText: '正在加载...',
        // nomoreText: '没有更多了'
      })
    },
    // 是否使用 scroll-view 包装内容
    useScrollView: {
      type: Boolean,
      default: true
    },
    // scroll-view 相关属性
    enableBackToTop: {
      type: Boolean,
      default: false
    },
    lowerThreshold: {
      type: [Number, String],
      default: 50
    },
    scrollTop: {
      type: [Number, String],
      default: 0
    }
  },
  data() {
    return {
      // 下拉刷新相关
      isRefreshing: false,
      refreshStatus: 'pull', // pull, release, refreshing
      refreshDistance: 0,
      startY: 0,
      currentY: 0,
      touching: false,
      
      // 动画相关
      contentTranslateY: 0
    }
  },
  emits: ['refresh', 'loadmore', 'scroll'],
  watch: {
    refreshing: {
      handler(newVal) {
        if (!newVal) {
          this.finishRefresh()
        } else {
          this.startRefresh()
        }
      }
    }
  },
  methods: {
    t,
    // 触摸开始
    onTouchStart(e) {
      if (this.isRefreshing) return
      
      this.touching = true
      this.startY = e.touches[0].pageY
      this.currentY = this.startY
      this.refreshStatus = 'pull'
    },
    
    // 触摸移动
    onTouchMove(e) {
      if (!this.touching || this.isRefreshing) return
      
      this.currentY = e.touches[0].pageY
      const diff = this.currentY - this.startY
      
      // 只有在顶部且下拉时才触发下拉刷新
      if (diff > 0 && this.isScrollViewAtTop()) {
        this.refreshDistance = Math.min(diff * this.damping, this.maxDistance)
        this.contentTranslateY = this.refreshDistance
        
        // 更新状态
        if (this.refreshDistance >= this.threshold) {
          this.refreshStatus = 'release'
        } else {
          this.refreshStatus = 'pull'
        }

        // 阻止默认滚动行为,防止触发页面级滚动
        e.preventDefault()
        e.stopPropagation()
      }
    },
    
    // 触摸结束
    onTouchEnd() {
      if (!this.touching) return
      
      this.touching = false
      
      if (this.refreshDistance >= this.threshold && !this.isRefreshing) {
        // 触发刷新
        this.startRefresh()
        this.$emit('refresh')
      } else {
        // 回弹
        this.resetRefresh()
      }
    },
    
    // 开始刷新
    startRefresh() {
      this.isRefreshing = true
      this.refreshStatus = 'refreshing'
      this.refreshDistance = this.threshold
      this.contentTranslateY = this.threshold
    },
    
    // 完成刷新
    finishRefresh() {
      this.isRefreshing = false
      this.refreshStatus = 'pull'
      this.resetRefresh()
    },
    
    // 重置刷新状态
    resetRefresh() {
      this.refreshDistance = 0
      this.contentTranslateY = 0
    },
    
    // 检查 scroll-view 是否在顶部
    isScrollViewAtTop() {
      // 这里可以更精确地判断,但简单起见直接返回 true
      // 实际项目中可能需要通过 scroll 事件获取 scrollTop 判断
      return true
    },
    
    // 处理滚动事件
    handleScroll(e) {
      this.$emit('scroll', e)
    },
    
    // 处理滚动到底部事件
    handleScrollToLower(e) {
      // 只有当 loadmore 状态为 loadmore 时才触发
      if (this.showLoadmore && this.loadmoreProps.status === 'loadmore') {
        this.$emit('loadmore')
      }
    }
  }
}
</script>

<style scoped lang="scss">
.u-pull-refresh {
  position: relative;
  height: 100%;
  overflow: hidden;
}

.refresh-area {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  overflow: hidden;
  transition: height 0.2s ease-out;
}

.refresh-content-wrapper {
  height: 100%;
  transition: transform 0.2s ease-out;
}

.scroll-wrapper {
  height: 100%;
}

.refresh-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding-bottom: 10px;
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid #f3f3f3;
  border-top: 2px solid #666;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.refresh-text {
  font-size: 14px;
  color: #666;
}
</style>