uniapp 实现一个底部悬浮面板

一. 效果

二. 设计目标

  • 地图不被完全遮挡:初始仅露出半屏地图,用户可继续拖动。

  • 内容可滚动:面板内部承载列表、卡片、工具栅格。

  • 交互自然:滚动顺滑、视觉层级清晰。

  • 适配微信小程序:避免使用不兼容语法。

  • 结构清晰、易扩展:后续可嵌入折叠动画、筛选控件等

三. 结构设计思路

层级 说明
底层 <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>
相关推荐
Aress"5 小时前
uniapp设置vuex公共值状态管理
javascript·vue.js·uni-app
song8546011345 小时前
uniapp如何集成第三方库
开发语言·uni-app
东芃93945 小时前
uniapp上传blob对象到后台
前端·javascript·uni-app
Aress"7 小时前
uniapp 生成二维码图片[APP+H5+小程序等 全端适配]
小程序·uni-app
2501_915921439 小时前
iOS 26 描述文件管理与开发环境配置 多工具协作的实战指南
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915909069 小时前
iOS 抓包实战 从原理到复现、定位与真机取证全流程
android·ios·小程序·https·uni-app·iphone·webview
2501_915106329 小时前
HBuilder 上架 iOS 应用全流程指南:从云打包到开心上架(Appuploader)上传的跨平台发布实践
android·ios·小程序·https·uni-app·iphone·webview
2501_9160074711 小时前
免费iOS加固方案指南
android·macos·ios·小程序·uni-app·cocoa·iphone
xuelong-ming13 小时前
uniapp vue3 点击跳转外部网页
vue.js·uni-app