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>
相关推荐
2501_916008899 小时前
全面介绍Fiddler、Wireshark、HttpWatch、SmartSniff和firebug抓包工具功能与使用
android·ios·小程序·https·uni-app·iphone·webview
webYin9 小时前
解决 Uni-App 运行到微信小程序时 “Socket合法域名校验出错” 问题
微信小程序·小程序·uni-app
奔跑的web.20 小时前
UniApp 路由导航守
前端·javascript·uni-app
特立独行的猫a21 小时前
主要跨端开发框架对比:Flutter、RN、KMP、Uniapp、Cordova,谁是未来主流?
flutter·uni-app·uniapp·rn·kmp·kuikly
万物得其道者成1 天前
UniApp 多端滑块验证码插件 zxj-slide-verify 实用指南
uni-app
蓝帆傲亦1 天前
支付宝小程序性能暴增秘籍:UniApp项目极限优化全攻略
小程序·uni-app
2501_916008892 天前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
QT.qtqtqtqtqt2 天前
uni-app小程序前端开发笔记(更新中)
前端·笔记·小程序·uni-app
喵喵虫2 天前
uniapp修改封装组件失败 styleIsolation
uni-app
游戏开发爱好者83 天前
日常开发与测试的 App 测试方法、查看设备状态、实时日志、应用数据
android·ios·小程序·https·uni-app·iphone·webview