uniapp 仿美团外卖详情页滑动面板组件[可自定义内容、自定义高度]
示例代码:
<!-- 组件 BottomSlidePanel.vue 代码 -->
<template><view class="bottom-slide-panel" v-if="visible"><!-- 背景遮罩 --><view class="overlay" :class="{ 'overlay-visible': showOverlay }" @tap="handleOverlayTap"></view><!-- 滑动面板 --><view class="panel" :class="{'panel-show': isShow,'panel-expanded': isExpanded}" :style="{// height: panelHeight + 'px',transform: getPanelTransform()}" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"><!-- 拖拽指示器 --><view class="drag-handle" @tap="toggle"><view class="drag-bar"></view></view><!-- 面板内容 --><view class="panel-content"><slot></slot></view></view></view>
</template><script>
export default {name: 'BottomSlidePanel',props: {// 是否显示面板visible: {type: Boolean,default: false},// 面板高度(px)height: {type: Number,default: 400},// 最小显示高度(收起时显示的高度)minHeight: {type: Number,default: 60},// 是否显示遮罩showMask: {type: Boolean,default: true},// 点击遮罩是否关闭maskClosable: {type: Boolean,default: true},// 初始是否展开defaultExpanded: {type: Boolean,default: true}},data() {return {isShow: false,isExpanded: false,showOverlay: false,panelHeight: 0,startY: 0,currentY: 0,isDragging: false,startTime: 0}},watch: {visible: {handler(newVal) {if (newVal) {this.show()} else {this.hide()}},immediate: true}},mounted() {this.panelHeight = this.heightthis.isExpanded = this.defaultExpanded},methods: {show() {this.isShow = truethis.$nextTick(() => {setTimeout(() => {if (this.defaultExpanded) {this.expand()} else {this.collapse()}}, 50)})},hide() {this.isShow = falsethis.showOverlay = falsethis.isExpanded = false},expand() {this.isExpanded = truethis.showOverlay = this.showMaskthis.$emit('expand')this.$emit('change', { expanded: true })},collapse() {this.isExpanded = falsethis.showOverlay = falsethis.$emit('collapse')this.$emit('change', { expanded: false })},toggle() {if (this.isExpanded) {this.collapse()} else {this.expand()}},handleOverlayTap() {if (this.maskClosable) {this.hide()this.$emit('update:visible', false)}},onTouchStart(e) {this.isDragging = truethis.startY = e.touches[0].clientYthis.currentY = this.startYthis.startTime = Date.now()},onTouchMove(e) {if (!this.isDragging) returne.preventDefault()this.currentY = e.touches[0].clientYconst deltaY = this.currentY - this.startY// 根据滑动方向和当前状态判断是否允许滑动if (this.isExpanded && deltaY > 0) {// 展开状态下向下滑动,允许收起return} else if (!this.isExpanded && deltaY < 0) {// 收起状态下向上滑动,允许展开return}},onTouchEnd() {if (!this.isDragging) returnthis.isDragging = falseconst deltaY = this.currentY - this.startYconst deltaTime = Date.now() - this.startTimeconst velocity = Math.abs(deltaY) / deltaTime// 快速滑动或滑动距离超过阈值时切换状态const threshold = 50const velocityThreshold = 0.3if (velocity > velocityThreshold || Math.abs(deltaY) > threshold) {if (deltaY > 0 && this.isExpanded) {// 向下滑动且当前展开,收起面板this.collapse()} else if (deltaY < 0 && !this.isExpanded) {// 向上滑动且当前收起,展开面板this.expand()}}},getPanelTransform() {if (!this.isShow) {return 'translateY(100%)'} else if (this.isExpanded) {return 'translateY(0)'} else {return `translateY(calc(100% - ${this.minHeight}px))`}}}
}
</script><style lang="scss" scoped>
.bottom-slide-panel {position: fixed;top: 0;left: 0;right: 0;bottom: 0;z-index: 1000;pointer-events: none;
}.overlay {position: absolute;top: 0;left: 0;right: 0;bottom: 0;// background-color: rgba(0, 0, 0, 0);transition: background-color 0.3s ease;pointer-events: none;&.overlay-visible {// background-color: rgba(0, 0, 0, 0.4);pointer-events: auto;}
}.panel {position: absolute;left: 0;right: 0;bottom: 0;background-color: #ffffff;border-radius: 32rpx 32rpx 0 0;box-shadow: 0 -4rpx 32rpx rgba(0, 0, 0, 0.1);transform: translateY(100%);transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);pointer-events: auto;display: flex;flex-direction: column;
}.drag-handle {display: flex;justify-content: center;align-items: center;height: 60rpx;padding: 16rpx 0;cursor: pointer;flex-shrink: 0;
}.drag-bar {width: 80rpx;height: 6rpx;background-color: #e5e5e5;border-radius: 3rpx;transition: background-color 0.2s ease;
}.drag-handle:active .drag-bar {background-color: #d0d0d0;
}.panel-content {flex: 1;padding: 0 32rpx 32rpx;-webkit-overflow-scrolling: touch;
}
</style>
使用示例:
<template><view class="demo-page"><!-- 模拟地图背景 --><view class="map-container"><image class="map-image" src="/static/map-bg.jpg" mode="aspectFill"></image><!-- 操作按钮 --><view class="control-buttons"><button @tap="showPanel" class="btn">显示面板</button><button @tap="hidePanel" class="btn">隐藏面板</button><button @tap="togglePanel" class="btn">切换状态</button></view></view><!-- 底部滑动面板 --><BottomSlidePanel :visible="panelVisible" :height="400" :minHeight="80" :showMask="true" :maskClosable="true":defaultExpanded="true" @update:visible="panelVisible = $event" @expand="onPanelExpand"@collapse="onPanelCollapse" @change="onPanelChange"><!-- 自定义面板内容 --><view class="panel-header"><text class="title">附近推荐</text></view><view class="content-section"><view class="info-card"><view class="card-icon">📍</view><view class="card-content"><text class="card-title">星巴克咖啡</text><text class="card-desc">距离您 200m · 营业中</text></view></view><view class="action-buttons"><button class="action-btn primary">导航</button><button class="action-btn secondary">收藏</button></view><view class="more-content"><text class="section-title">更多推荐</text><view class="item-list"><view class="list-item" v-for="item in 5" :key="item"><text class="item-name">推荐地点 {{ item }}</text><text class="item-distance">{{ item * 100 }}m</text></view></view></view></view></BottomSlidePanel></view>
</template><script>
import BottomSlidePanel from '@/components/BottomSlidePanel.vue'export default {components: {BottomSlidePanel},data() {return {panelVisible: false}},methods: {showPanel() {this.panelVisible = true},hidePanel() {this.panelVisible = false},togglePanel() {this.panelVisible = !this.panelVisible},onPanelExpand() {console.log('面板展开')},onPanelCollapse() {console.log('面板收起')},onPanelChange(e) {console.log('面板状态改变:', e.expanded)}}
}
</script><style lang="scss" scoped>
.demo-page {height: 100vh;position: relative;
}.map-container {width: 100%;height: 100%;position: relative;background-color: #f0f0f0;
}.map-image {width: 100%;height: 100%;
}.control-buttons {position: absolute;top: 100rpx;left: 32rpx;right: 32rpx;display: flex;gap: 20rpx;
}.btn {flex: 1;height: 80rpx;background-color: #007aff;color: white;border: none;border-radius: 8rpx;font-size: 28rpx;
}.panel-header {padding: 0 0 32rpx 0;border-bottom: 1rpx solid #f0f0f0;
}.title {font-size: 36rpx;font-weight: 600;color: #333;
}.content-section {padding-top: 32rpx;
}.info-card {display: flex;align-items: center;padding: 24rpx;background-color: #fff7e6;border-radius: 12rpx;border: 1rpx solid #ffd591;margin-bottom: 32rpx;
}.card-icon {font-size: 48rpx;margin-right: 24rpx;
}.card-content {flex: 1;
}.card-title {display: block;font-size: 32rpx;font-weight: 600;color: #333;margin-bottom: 8rpx;
}.card-desc {font-size: 26rpx;color: #666;
}.action-buttons {display: flex;gap: 24rpx;margin-bottom: 48rpx;
}.action-btn {flex: 1;height: 88rpx;border-radius: 12rpx;font-size: 32rpx;border: none;&.primary {background-color: #ff6b35;color: white;}&.secondary {background-color: #f5f5f5;color: #333;}
}.more-content {margin-top: 32rpx;
}.section-title {display: block;font-size: 32rpx;font-weight: 600;color: #333;margin-bottom: 24rpx;
}.item-list {display: flex;flex-direction: column;gap: 24rpx;
}.list-item {display: flex;justify-content: space-between;align-items: center;padding: 24rpx 0;border-bottom: 1rpx solid #f0f0f0;
}.item-name {font-size: 30rpx;color: #333;
}.item-distance {font-size: 26rpx;color: #999;
}
</style>
</template>
效果展示: