uniapp 实现一个底部悬浮面板
一. 效果
二. 设计目标
地图不被完全遮挡:初始仅露出半屏地图,用户可继续拖动。
内容可滚动:面板内部承载列表、卡片、工具栅格。
交互自然:滚动顺滑、视觉层级清晰。
适配微信小程序:避免使用不兼容语法。
结构清晰、易扩展:后续可嵌入折叠动画、筛选控件等
三. 结构设计思路
| 层级 | 说明 |
|---|---|
| 底层 | <map> 组件(高德/腾讯地图) |
| 中层 | 搜索栏 + 悬浮按钮(FAB) |
| 顶层 | 可滚动浮层(scroll-view 承载) |
浮层的核心是一个 scroll-view 包裹的「透明占位 + 面板」结构:
<scroll-view class="overlay-scroll" :scroll-y="true"><!-- 透明 spacer 决定初始地图可见高度 --><div class="top-spacer" :style="{ height: spacerPx + 'px' }">…</div><!-- 真正的工具面板 --><div class="tool-sheet"><div class="sheet-header"><div class="grabber"></div></div><div class="sheet-body">…</div></div>
</scroll-view>
这里的 top-spacer 负责“撑开”地图初始露出的部分。当用户向上滚动时,它被滚走,tool-sheet 便自然上移覆盖地图。
这种结构无需复杂的 JS 拖拽计算,就能通过 滚动惯性 实现“半屏到全屏”的视觉切换。
四. 交互与布局逻辑
1. scroll-view 占满全屏
.overlay-scroll {
position: absolute;
inset: 0;
z-index: 10;
background: transparent;
}
2.top-spacer 控制地图漏出比例
const sys = uni.getSystemInfoSync()
const spacerPx = Math.floor(sys.windowHeight * 0.5)
设置屏高50%,即初始漏出地图半屏
3. tool-sheet(面板)
.tool-sheet {
background: #fff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
min-height: 70vh;
box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
}
五. 完整代码
<template><jc-Layout><!-- 地图底层 --><mapclass="map":latitude="center.lat":longitude="center.lng":scale="12":enable-3D="true":enable-overlooking="false":enable-satellite="false":show-location="true":markers="markers"><!-- 顶部搜索栏 --><view class="search-bar"><view class="search-input"><wd-inputv-model="keyword"clearableborder="none"custom-input-class="ipt"no-border><template #prefix><view class="search-icon"><jc-icon size="28rpx" name="sousuo1" /></view></template></wd-input></view><view class="mr-2"><wd-buttontype="info"plain:round="false"size="small"@click="onSearch">搜索</wd-button></view></view></map><!-- 可滚动浮层 --><scroll-viewclass="overlay-scroll":scroll-y="true":enhanced="true":bounces="false":show-scrollbar="false"><!-- Spacer 控制地图露出比例 --><view class="top-spacer" :style="{ height: spacerPx + 'px' }"><jc-fab :absolute="true" icon="fuhao-tuceng" top="600" left="24" /><jc-fab:absolute="true"icon="shuaxin"top="700"left="24"@click="refresh"/><jc-fab :absolute="true" icon="dingwei" top="700" right="24" /></view><!-- 工具面板 --><view class="tool-sheet"><!-- 标题头部 --><view class="sheet-header"><view class="grabber" /><view class="title"><wd-icon name="truck" size="18px" /></view></view><!-- 内容区 --><view class="sheet-body"><view class="tool-grid"><view v-for="n in 15" :key="n" class="tool-item"><view class="icon-wrap"><jc-icon name="left" size="20px" /></view><view class="label">功能{{ n }}</view></view></view><!-- 列表 --><view class="detail-list"><wd-cell-group border><wd-cellv-for="n in 14":key="n"title="占位字段"value="占位值"/></wd-cell-group></view></view></view></scroll-view></jc-Layout>
</template><script setup lang="ts">
definePage({style: {navigationStyle: 'custom',navigationBarTitleText: '车辆工具',},
})const keyword = ref('')
const center = ref({ lat: 23.1291, lng: 113.2644 })
const markers = ref<any[]>([])const sys = uni.getSystemInfoSync()
const spacerPx = Math.floor(sys.windowHeight * 0.5) // 露出地图 50%function onSearch() {console.log('搜索关键词:', keyword.value)
}
function refresh() {console.log('刷新数据')
}
</script><style scoped lang="scss">
/* 搜索栏 */
.search-bar {background: #ffffff;height: 100rpx;width: 100%;position: absolute;top: 0rpx;z-index: 10;display: flex;gap: 12rpx;align-items: center;.search-input {width: 95%;:deep(.ipt) {background: #f7f8fa;padding: 0 50rpx;height: 60rpx !important;}}.search-icon {position: absolute;left: 20rpx;top: 50%;transform: translateY(-50%);}
}/* 地图底层 */
.map {position: relative;inset: 0;width: 100%;height: 100%;z-index: 1;
}/* 滚动浮层 */
.overlay-scroll {position: absolute;inset: 0;z-index: 10;background: transparent;.top-spacer {position: relative;width: 100%;}.tool-sheet {background: #fff;border-top-left-radius: 24rpx;border-top-right-radius: 24rpx;box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.12);min-height: 70vh;display: flex;flex-direction: column;overflow: hidden;}.sheet-header {padding-top: 16rpx;padding-bottom: 12rpx;.grabber {width: 88rpx;height: 8rpx;border-radius: 8rpx;background: #e5e7eb;margin: 0 auto 14rpx;}.title {display: flex;align-items: center;gap: 12rpx;padding: 0 24rpx;.txt {font-size: 30rpx;font-weight: 600;color: #111;}}}.sheet-body {flex: 1;min-height: 0;padding-bottom: env(safe-area-inset-bottom);}.tool-grid {width: 100%;display: flex;flex-wrap: wrap;justify-content: flex-start;padding: 10rpx;box-sizing: border-box;}.tool-item {width: 20%;display: flex;flex-direction: column;align-items: center;justify-content: center;margin-bottom: 32rpx;.icon-wrap {width: 86rpx;height: 86rpx;border-radius: 50%;background: #e9fbf9;display: flex;align-items: center;justify-content: center;}.label {margin-top: 10rpx;font-size: 24rpx;color: #333;}}.detail-list {padding: 0 16rpx 24rpx;}
}
</style>
