| New file |
| | |
| | | <template> |
| | | <view class="panel-container" :style="{ |
| | | transform: `translateY(${translateY}px)`, |
| | | transition: isDragging ? 'none' : 'transform 0.3s ease', |
| | | }" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"> |
| | | <!-- 顶部拖拽提示条 --> |
| | | <view class="drag-bar"></view> |
| | | |
| | | <!-- 搜索框(带毛玻璃) --> |
| | | <view class="search-box" :style="searchBoxStyle"> |
| | | <slot name="searchBox"></slot> |
| | | </view> |
| | | |
| | | <!-- 内容区 --> |
| | | <scroll-view scroll-y class="panel-content"> |
| | | <view class="dummy-content"> |
| | | <text>这里可以放搜索结果、推荐地点等内容</text> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | onMounted, |
| | | watch |
| | | } from 'vue' |
| | | |
| | | const props = defineProps({ |
| | | isSelectInput: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | searchVal: { |
| | | type: String, |
| | | default: '' |
| | | }, |
| | | }) |
| | | |
| | | // 获取屏幕高度 |
| | | let screenHeight = 0 |
| | | |
| | | // 拖拽状态 |
| | | const startY = ref(0) |
| | | const currentY = ref(0) |
| | | const translateY = ref(0) |
| | | const isDragging = ref(false) |
| | | |
| | | const initialPosition = ref(0.8) |
| | | const oneSlidePosition = ref(0.6) |
| | | const twoSlidePosition = ref(0.1) |
| | | |
| | | // 毛玻璃效果 |
| | | const searchBoxStyle = computed(() => { |
| | | return translateY.value <= screenHeight * oneSlidePosition.value ? |
| | | { |
| | | backdropFilter: 'blur(10px)', |
| | | background: 'rgba(255, 255, 255, 0.7)', |
| | | } : |
| | | { |
| | | background: '#fff', |
| | | } |
| | | }) |
| | | |
| | | watch([ |
| | | () => props.isSelectInput, |
| | | () => props.searchVal, |
| | | ], ([newIsSelectInput, newSearchVal]) => { |
| | | if (newIsSelectInput) { |
| | | translateY.value = screenHeight * twoSlidePosition.value |
| | | } |
| | | |
| | | if (!newIsSelectInput && !newSearchVal) { |
| | | translateY.value = screenHeight * initialPosition.value |
| | | } |
| | | }) |
| | | |
| | | // 获取屏幕高度 |
| | | onMounted(() => { |
| | | const info = uni.getSystemInfoSync() |
| | | screenHeight = info.windowHeight |
| | | |
| | | // #ifdef APP-PLUS |
| | | initialPosition.value = 0.8 |
| | | // #endif |
| | | |
| | | // #ifdef H5 |
| | | initialPosition.value = 0.95 |
| | | |
| | | if (screenHeight < 500) { |
| | | initialPosition.value = 0.86 |
| | | } |
| | | |
| | | if (screenHeight > 575) { |
| | | initialPosition.value = 0.85 |
| | | } |
| | | |
| | | if (screenHeight > 642) { |
| | | initialPosition.value = 0.865 |
| | | } |
| | | // #endif |
| | | |
| | | translateY.value = screenHeight * initialPosition.value |
| | | }) |
| | | |
| | | // 拖拽开始 |
| | | const onTouchStart = (e) => { |
| | | // 只在拖拽时,阻止页面滚动 |
| | | startY.value = e.touches[0].clientY |
| | | currentY.value = translateY.value |
| | | isDragging.value = true |
| | | } |
| | | |
| | | // 拖拽移动 |
| | | const onTouchMove = (e) => { |
| | | // 只在拖拽时,阻止页面滚动 |
| | | if (isDragging.value) { |
| | | e.preventDefault() // 阻止滚动行为 |
| | | e.stopPropagation() // 阻止事件冒泡 |
| | | } |
| | | |
| | | const delta = e.touches[0].clientY - startY.value |
| | | let nextY = currentY.value + delta |
| | | |
| | | // 限制拖拽范围 [0, 90% 屏幕高度] |
| | | nextY = Math.min(screenHeight * (1 - twoSlidePosition.value), Math.max(0, nextY)) |
| | | |
| | | translateY.value = nextY |
| | | } |
| | | |
| | | // 拖拽结束后自动吸附 |
| | | const onTouchEnd = () => { |
| | | isDragging.value = false |
| | | const percentFromTop = 100 - (translateY.value / screenHeight) * 100 |
| | | |
| | | if (percentFromTop < 20) translateY.value = screenHeight * initialPosition.value |
| | | else if (percentFromTop < 40) translateY.value = screenHeight * oneSlidePosition.value |
| | | else if (percentFromTop < 75) translateY.value = screenHeight * oneSlidePosition.value |
| | | else if (percentFromTop < 90) translateY.value = screenHeight * twoSlidePosition.value |
| | | else translateY.value = screenHeight * twoSlidePosition.value |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .panel-container { |
| | | position: fixed; |
| | | bottom: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 90vh; |
| | | background: #fff; |
| | | border-top-left-radius: 20rpx; |
| | | border-top-right-radius: 20rpx; |
| | | box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.15); |
| | | display: flex; |
| | | flex-direction: column; |
| | | z-index: 997; |
| | | touch-action: none; |
| | | /* 禁止默认的滚动行为 */ |
| | | } |
| | | |
| | | .drag-bar { |
| | | width: 80rpx; |
| | | height: 8rpx; |
| | | background: #ccc; |
| | | border-radius: 4rpx; |
| | | margin: 16rpx auto; |
| | | } |
| | | |
| | | .search-box { |
| | | padding: 0 24rpx; |
| | | margin-bottom: 12rpx; |
| | | border-radius: 40rpx; |
| | | transition: backdrop-filter 0.3s ease, background 0.3s ease; |
| | | } |
| | | |
| | | .search-input { |
| | | width: 100%; |
| | | height: 80rpx; |
| | | border-radius: 40rpx; |
| | | background: #f2f2f2; |
| | | padding: 0 24rpx; |
| | | font-size: 28rpx; |
| | | border: none; |
| | | outline: none; |
| | | } |
| | | |
| | | .panel-content { |
| | | flex: 1; |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .dummy-content { |
| | | text-align: center; |
| | | color: #999; |
| | | margin-top: 100rpx; |
| | | } |
| | | </style> |