一. 效果
二. 设计目标
-
地图不被完全遮挡:初始仅露出半屏地图,用户可继续拖动。
-
内容可滚动:面板内部承载列表、卡片、工具栅格。
-
交互自然:滚动顺滑、视觉层级清晰。
-
适配微信小程序:避免使用不兼容语法。
-
结构清晰、易扩展:后续可嵌入折叠动画、筛选控件等
三. 结构设计思路
| 层级 | 说明 |
|---|---|
| 底层 | <map> 组件(高德/腾讯地图) |
| 中层 | 搜索栏 + 悬浮按钮(FAB) |
| 顶层 | 可滚动浮层(scroll-view 承载) |
浮层的核心是一个 scroll-view 包裹的「透明占位 + 面板」结构:
html
<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;
}
五. 完整代码
html
<template>
<jc-Layout>
<!-- 地图底层 -->
<map
class="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-input
v-model="keyword"
clearable
border="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-button
type="info"
plain
:round="false"
size="small"
@click="onSearch"
>
搜索
</wd-button>
</view>
</view>
</map>
<!-- 可滚动浮层 -->
<scroll-view
class="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-cell
v-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>