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
273 lines
5.8 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)` }">
|
|
<!-- 顶部拖拽条 -->
|
|
<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>
|