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.

273 lines
5.8 KiB

6 days ago
<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)` }">
<!-- 顶部拖拽条 -->
<view class="header" @touchstart.stop="onStart" @touchmove.stop.prevent="onMove" @touchend="onEnd">
<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.windowHeight;
},
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.isFullScreen) { // translateY = 0
if (moveY > 50) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
// 往上拉(基本不会)→ 保持全屏
if (moveY < -50) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
}
// ---------------------------
// 2. 在半屏状态
// ---------------------------
if (!this.isFullScreen) {
if (moveY < -80) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
// 下拉轻微 → 保持半屏
if (moveY > 0 && moveY < 50) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
}
// ---------------------------
// 3. 如果下拉超过阈值(且不是全屏)→ 关闭
// ---------------------------
if (moveY > 50 && !this.isFullScreen) {
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: 60rpx;
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>