You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

229 lines
5.8 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div
ref="pullContainer"
class="pull-refresh-container"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<!-- 下拉刷新区域 -->
<div
ref="pullHeader"
class="pull-header"
:style="{ transform: `translateY(${pullDistance}px)` }"
>
<div class="pull-text">
<template v-if="refreshStatus === 'pull'">
{{ pullText }}
</template>
<template v-else-if="refreshStatus === 'release'">
{{ releaseText }}
</template>
<template v-else-if="refreshStatus === 'refreshing'"> <span class="spinner"></span> {{ refreshingText }} </template>
</div>
</div>
<!-- 内容区域 -->
<div class="content">
<slot />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useBodyClass } from '@/hooks/useBodyClass';
useBodyClass('body-overflow-hidden');
// ==================== Props ====================
const props = defineProps({
pullThreshold: {
type: Number,
default: 80,
},
pullText: {
type: String,
default: '下拉刷新',
},
releaseText: {
type: String,
default: '释放刷新',
},
refreshingText: {
type: String,
default: '刷新中...',
},
onRefresh: {
type: Function,
required: true,
},
});
// ==================== Refs ====================
const pullContainer = ref(null);
const pullHeader = ref(null);
// ==================== State ====================
const pullDistance = ref(0);
const startY = ref(0);
const isDragging = ref(false);
const refreshStatus = ref('idle');
// ==================== Computed ====================
const isPulling = computed(() => pullDistance.value > 0);
// ==================== Methods ====================
const handleTouchStart = (e) => {
startY.value = e.touches[0].clientY;
isDragging.value = true;
pullDistance.value = 0;
};
const handleTouchMove = (e) => {
if (!isDragging.value) return;
const currentY = e.touches[0].clientY;
const deltaY = currentY - startY.value;
// 只允许向下拉
if (deltaY <= 0) {
pullDistance.value = 0;
return;
}
// 限制最大下拉距离(可选)
const maxPull = props.pullThreshold * 1.5;
pullDistance.value = Math.min(deltaY, maxPull);
// 更新状态
if (pullDistance.value >= props.pullThreshold) {
refreshStatus.value = 'release';
} else {
refreshStatus.value = 'pull';
}
// 手动控制内容区域的 transform模拟弹性滚动
const content = pullContainer.value.querySelector('.content');
if (content) {
content.style.transform = `translateY(${pullDistance.value}px)`;
content.style.transition = 'none';
}
};
const handleTouchEnd = async () => {
// debugger
if (!isDragging.value) return;
isDragging.value = false;
if (pullDistance.value >= props.pullThreshold) {
// 触发刷新
refreshStatus.value = 'refreshing';
// 保持在阈值位置,不回弹
const targetDistance = props.pullThreshold;
// pullDistance.value = targetDistance
pullDistance.value = 60;
const content = pullContainer.value.querySelector('.content');
if (content) {
content.style.transition = 'transform 0s ease-out';
content.style.transform = 'translateY(60px)';
}
try {
await props.onRefresh();
} catch (error) {
console.error('刷新失败:', error);
} finally {
// 刷新完成,才开始回弹
pullDistance.value = 0;
refreshStatus.value = 'idle';
const content = pullContainer.value.querySelector('.content');
if (content) {
content.style.transition = 'transform 0.3s ease-out';
content.style.transform = 'translateY(0px)';
}
}
} else {
// 未达到阈值,直接回弹到顶部
pullDistance.value = 0;
refreshStatus.value = 'idle';
const content = pullContainer.value.querySelector('.content');
if (content) {
content.style.transition = 'transform 0.3s ease-out';
content.style.transform = 'translateY(0px)';
}
}
};
// ==================== Lifecycle ====================
onMounted(() => {
const header = pullHeader.value;
if (header) {
header.style.height = `${header.clientHeight}px`;
}
});
onUnmounted(() => {
// 清理事件监听Vue 3 中通常不需要手动清除,但安全起见)
});
</script>
<style scoped>
.pull-refresh-container {
position: relative;
height: 100vh;
overflow: hidden;
touch-action: none;
margin-top: -60px !important;
}
.pull-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
z-index: 10;
will-change: transform;
}
.pull-text {
font-size: 14px;
color: #999;
text-align: center;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ccc;
border-top-color: #007aff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 6px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.content {
padding-top: 60px;
height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
will-change: transform;
transition: transform 0.3s ease-out;
}
</style>