123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- <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">下拉刷新</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">释放刷新</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">正在刷新...</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>
- 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: {
- // 触摸开始
- 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>
|