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

278 lines
5.3 KiB

<template>
<view class="drawer-container">
<!-- 蒙层 -->
<!-- <view
class="mask"
:class="{ show: visibleInner && isFullScreen }"
:style="{ top: isFullScreen ? '0' : halfY + 'px' }"
@click="close"
/> -->
<!-- 抽屉 -->
<view
class="drawer"
:class="{
dragging: isDragging
}"
:style="{ transform: `translateY(${translateY}px)` }"
@touchstart="onStart"
@touchmove.stop.prevent="onMove"
@touchend="onEnd"
>
<!-- 顶部拖拽条 -->
<view class="header" @touchstart.stop="onStart">
<view class="bar"></view>
</view>
<!-- 内容 -->
<view class="content">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: "PopupDrawer",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
visibleInner: false,
// 拖动状态
isDragging: false,
startY: 0,
currentY: 0,
// 位置
windowHeight: 0,
halfY: 0,
fullY: 0,
translateY: 0,
startTranslateY: 0,
// 显示状态
isFullScreen: false
};
},
watch: {
visible: {
immediate: true,
handler(v) {
if (v) {
this.open();
} else {
this.close();
}
}
}
},
created() {
const res = uni.getSystemInfoSync();
this.windowHeight = res.windowHeight;
this.halfY = this.windowHeight * 0.5;
this.fullY = 0;
this.translateY = this.halfY;
},
methods: {
/** 打开 */
open() {
this.visibleInner = true;
this.isFullScreen = false; // 默认打开为半屏
this.$nextTick(() => {
this.animateTo(this.halfY);
});
},
/** 关闭 */
close() {
this.isFullScreen = false;
this.animateTo(this.windowHeight, () => {
this.visibleInner = false;
this.$emit("update:visible", false);
this.$emit("close");
});
},
/** 手势开始 */
onStart(e) {
// 阻止默认行为和事件冒泡
e.preventDefault();
e.stopPropagation();
this.isDragging = true;
this.startY = e.touches[0].clientY;
this.startTranslateY = this.translateY;
},
/** 手势移动 */
onMove(e) {
// 阻止默认行为和事件冒泡
e.preventDefault();
e.stopPropagation();
if (!this.isDragging) return;
const moveY = e.touches[0].clientY - this.startY;
let newY = this.startTranslateY + moveY;
// 限制范围
if (newY < 0) newY = 0;
if (newY > this.windowHeight) newY = this.windowHeight;
this.translateY = newY;
},
/** 手势结束(吸附逻辑) */
onEnd(e) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const moveY = e.changedTouches[0].clientY - this.startY;
// ---------------------------
// 1. 在全屏状态
// ---------------------------
if (this.translateY === this.fullY) { // translateY = 0
// 往下拉超过 80 → 吸附到半屏,而不是关闭
if (moveY > 80) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
// 往上拉(基本不会)→ 保持全屏
if (moveY < -50) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
}
// ---------------------------
// 2. 在半屏状态
// ---------------------------
if (this.translateY >= this.halfY - 10 && this.translateY <= this.halfY + 10) {
// 上拉 → 全屏
if (moveY < -80) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
// 下拉轻微 → 保持半屏
if (moveY > 0 && moveY < 120) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
}
// ---------------------------
// 3. 如果下拉超过阈值(且不是全屏)→ 关闭
// ---------------------------
if (moveY > 120) {
this.isFullScreen = false;
return this.close();
}
// ---------------------------
// 4. 自动吸附最近位置
// ---------------------------
const points = [this.fullY, this.halfY];
let nearest = points.reduce((prev, curr) =>
Math.abs(curr - this.translateY) < Math.abs(prev - this.translateY) ? curr : prev
);
this.isFullScreen = (nearest === this.fullY);
this.animateTo(nearest);
},
/** 动画过渡(使用 RAF,不卡顿) */
animateTo(targetY, callback) {
// 小程序动画对象
const animation = uni.createAnimation({
duration: 250,
timingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)"
});
animation.translateY(targetY).step();
this.animationData = animation.export();
this.translateY = targetY;
if (callback) {
setTimeout(callback, 260);
}
}
}
};
</script>
<style lang="scss" scoped>
.drawer-container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 默认不阻挡交互 */
z-index: 9999;
.mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
opacity: 0;
transition: opacity 0.25s;
pointer-events: auto; /* 蒙层接收交互 */
&.show {
opacity: 1;
}
}
.drawer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
transition: transform 0.25s ease-out;
pointer-events: auto; /* 抽屉部分接收交互 */
&.dragging {
transition: none !important;
}
.header {
width: 100%;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
.bar {
width: 120rpx;
height: 12rpx;
border-radius: 6rpx;
background: #ccc;
}
}
.content {
height: calc(100% - 80rpx);
overflow-y: auto;
}
}
}
</style>