-
Notifications
You must be signed in to change notification settings - Fork 32
Open
Description
组件代码
<template>
<!-- 遮罩层 -->
<view :style="{height:isExpanded?`${heightPx}px`:'0'}"
class="fixed top-0 left-0 right-0 bottom-0 z-[0] bg-transparent bg-opacity-30 transition-opacity duration-150 ease-in-out "
@click="closePanel"></view>
<view class="fixed bottom-0 left-0 right-0" :class="{ 'transition-all duration-150 ease-out': !isDragging }"
:style="{ bottom: bottomHeight + 'px', height: `${Math.max(minHeight, maxHeight - translateY)}rpx` }">
<!-- 浮动面板 -->
<view class="absolute bottom-0 left-0 right-0 z-[30] rounded-t-[40rpx] z-[30] shadow-2xl pointer-events-auto"
:class="{ 'transition-transform duration-150 ease-in-out': !isDragging }"
:style="{ transform: `translateY(${translateY}rpx)`, height: `${maxHeight}rpx` }">
<!-- 拖拽头部 -->
<view @touchstart.prevent="onTouchStart" @touchmove.prevent="onTouchMove" @touchend.prevent="onTouchEnd">
<slot name="head">
<view
class="relative flex flex-col items-center justify-center bg-white rounded-t-[40rpx] h-[60rpx] py-[20rpx] border-b border-gray-100">
<!-- 拖拽指示器 -->
<view class="bg-gray-300 rounded-full w-[80rpx] h-[8rpx]"></view>
</view>
</slot>
</view>
<!-- 面板内容 -->
<scroll-view class="flex-1 p-[40rpx] bg-white" @touchstart.stop @touchmove.stop @touchend.stop>
<slot></slot>
</scroll-view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
defineOptions({
name: "float-panel"
});
type Props = {
maxHeight ?: number;
minHeight ?: number;
bottomHeight ?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxHeight: 1200, // 最大高度 rpx
minHeight: 60, // 最小高度(头部高度)rpx
bottomHeight: 0
})
const heightPx = computed(() => {
let window = uni.getWindowInfo()
return window.screenHeight - props.bottomHeight
})
const emits = defineEmits(['getStatus'])
// 解构props以便在模板中使用
const { maxHeight, minHeight, bottomHeight } = props
// 面板状态
const translateY = ref(props.maxHeight - props.minHeight) // 初始位置
const isExpanded = ref(false)
const isDragging = ref(false)
// 触摸相关
const startY = ref(0)
const startTranslateY = ref(0)
const currentY = ref(0)
// 记录时间戳
const startTime = ref(0)
const endTime = ref(0)
// 计算面板是否展开
const panelExpanded = computed(() => {
return translateY.value < (props.maxHeight - props.minHeight) / 2
})
// 展开面板
function expandPanel() {
translateY.value = 0
isExpanded.value = true
emits('getStatus', true)
}
// 收起面板
function collapsePanel() {
translateY.value = props.maxHeight - props.minHeight
isExpanded.value = false
emits('getStatus', false)
}
// 关闭面板(点击遮罩)
function closePanel() {
collapsePanel()
}
// 触摸开始
function onTouchStart(e : UniTouchEvent) {
isDragging.value = true
startY.value = e.touches[0].clientY
startTranslateY.value = translateY.value
currentY.value = startY.value
startTime.value = Date.now()
}
// 触摸移动
function onTouchMove(e : UniTouchEvent) {
if (!isDragging.value) return
currentY.value = e.touches[0].clientY
const deltaY = currentY.value - startY.value
let newTranslateY = startTranslateY.value + deltaY
// 限制拖拽范围
const maxTranslateY = props.maxHeight - props.minHeight
const minTranslateY = 0
// 严格限制在范围内,不允许超出
if (newTranslateY > maxTranslateY) {
newTranslateY = maxTranslateY
} else if (newTranslateY < minTranslateY) {
newTranslateY = minTranslateY
}
translateY.value = newTranslateY
// 实时更新isExpanded状态用于遮罩层显示
const threshold = (props.maxHeight - props.minHeight) * 0.3
isExpanded.value = newTranslateY < threshold
}
// 计算滑动速度
function calculateVelocity() : number {
const deltaY = currentY.value - startY.value
const deltaTime = endTime.value - startTime.value
return deltaY / Math.max(deltaTime, 16) // 最小16ms防止除零
}
// 切换面板状态(点击头部)
function togglePanel() {
if (isExpanded.value) {
collapsePanel()
} else {
expandPanel()
}
}
// 触摸结束
function onTouchEnd(e : UniTouchEvent) {
if (!isDragging.value) return
isDragging.value = false
endTime.value = Date.now()
const deltaY = Math.abs(currentY.value - startY.value)
const deltaTime = endTime.value - startTime.value
// 判断是否为点击(移动距离小且时间短)
if (deltaY < 10 && deltaTime < 300) {
// 点击行为,切换面板状态
togglePanel()
return
}
const velocity = calculateVelocity()
const threshold = (props.maxHeight - props.minHeight) / 2
// 根据速度和位置决定最终状态
if (velocity > 0.5) {
// 向下滑动,收起面板
collapsePanel()
} else if (velocity < -0.5) {
// 向上滑动,展开面板
expandPanel()
} else if (translateY.value > threshold) {
// 位置超过阈值,收起面板
collapsePanel()
} else {
// 位置未超过阈值,展开面板
expandPanel()
}
}
// 暴露方法给父组件
defineExpose({
expandPanel,
collapsePanel,
isExpanded: computed(() => isExpanded.value)
})
onMounted(() => {
// 初始化状态
isExpanded.value = false
})
</script>
Metadata
Metadata
Assignees
Labels
No labels