UniApp 页面布局组件设计与实现:打造灵活可复用的页面结构Layout 组件

在现代移动应用开发中,一个良好的页面布局组件可以显著提高开发效率,保持应用风格一致,并解决各种设备适配问题。本文将深入分析一个基于 UniApp 的页面布局组件实现,它集成了头部导航、内容区域、底部栏以及安全区域适配等功能。

一.组件代码

<template>
  <div class="page-container">
    <!-- 头部 -->
    <div class="page-header">
      <slot name="header">
        <Navbar :title="title ?? defaultTitle" :fixed="fixedHeader"></Navbar>
      </slot>
      <!-- 占位  如果固定在顶部-->
      <div v-if="fixedHeader">
        <!-- 状态栏占位, 高度与状态栏相等 -->
        <fui-status-bar></fui-status-bar>
        <!-- 头部导航栏占位 -->
        <div style="width:100%;height:44px;"></div>
      </div>
    </div>
    <!-- 内容 -->
    <div class="page-content">
      <slot></slot>
    </div>
    <!-- 空白占位符, 用来给固定的底部预留出空间, 确保页面内容在滚动底部时不会被页脚遮住 -->
    <!--  底部页脚通过position:fixed固定在页面底部,从文档流中脱离,所以才需要占位符 -->
    <div class="placeholder" :style="{ height: `${bottomHeight}px` }"></div>
    <!-- 底部 -->
    <div class="page-footer">
      <slot name="footer"></slot>
      <!-- 底部安全区域,用于适配iphonex 下面有一条黑线等机型底部安全区域 !-->
      <fui-safe-area v-if="!isTabbarPage && showFixedSafeArea"></fui-safe-area>
    </div>
    <!-- 压屏窗 -->
    <Landscape ref="landscape"></Landscape>
    <Dialog ref="dialog"></Dialog>
    <fui-toast ref="toast"></fui-toast>
  </div>
</template>

<script lang="ts" setup>
  //获取当前页面信息     rpx与px相互转换
  import { getCurrentPage, upx2px } from '@/utils/uniTools'
  //引入管理应用的配置信息,页面信息,标签栏配置和系统信息
  import { useConfigStore } from '@/store'
  //获取当前组件实例对象
  import { getCurrentInstance } from 'vue'



  interface IProp {
    //页面标题
    title ?: string
    //是否使用固定头部
    fixedHeader ?: boolean
    //是否显示底部安全区域适配
    showFixedSafeArea ?: boolean
  }

  const props = withDefaults(defineProps<IProp>(), {
    fixedHeader: true,
    showFixedSafeArea: true
  })
  //标题
  const defaultTitle = ref('')
  //footer底部高度
  const bottomHeight = ref(0)
  //悬浮按钮,菜单按钮的宽度
  const navbarWidth = ref(upx2px(180))
  //接收应用程序配置信息仓库
  const configStore = useConfigStore()

  const landscape = ref()
  const dialog = ref()
  const toast = ref()

  // 当前页面是否tabbar页面
  const isTabbarPage = computed(() => configStore.tabbarPaths.includes(getCurrentPage().route!) ?? false)

  // 获取标题
  const getDefaultTitle = () => {
    //获取没个页面的信息
    const pagesMap = configStore.pagesMap
    //获取当前页面信息
    const page = getCurrentPage()
    defaultTitle.value = page.route ? pagesMap[page.route]?.style?.navigationBarTitleText ?? '' : ''
  }
  getDefaultTitle()

  //获取当前组件的实例对象
  const instance = getCurrentInstance()


  //获取指定模块的节点信息 
  const getRect = (module : 'header' | 'footer' | 'content') => {
    return new Promise<UniApp.NodeInfo>((resolve) => {
      if (!['header', 'footer', 'content'].includes(module)) {
        resolve({})
      }
      //查询DOM元素并获取信息,
      const query = uni.createSelectorQuery().in(instance);
      // 获取到所选元素的边界信息,位置,大小等
      query.select(`.page-${module}`).boundingClientRect(data => {
        resolve(data as UniApp.NodeInfo)
      }).exec();
    })
  }

  //获取到footer内容的高度 
  const getFooterHeight = async () => {
    const data = await getRect('footer')
    bottomHeight.value = data?.height ?? 0
  }
  //获取中间的高度
  const getheaderHeight = async () => {
    const data = await getRect('content')
    // bottomHeight.value = data?.height ?? 0
  }

  //获取菜单按钮(悬浮按钮)的宽度 
  const getFloatingButtonWidth = () => {

    const { width } = uni.getMenuButtonBoundingClientRect()
    navbarWidth.value = width
  }


  // 注册组件
  const setupDefaultComponents = () => {
    //获取小程序全局应用实例
    const app = getApp()
    app.globalData!.landscape = landscape.value
    app.globalData!.dialog = dialog.value
    app.globalData!.toast = toast.value
  }

  onMounted(() => {
    //获取悬浮按钮的宽度
    getFloatingButtonWidth()
    const systemInfo = uni.getSystemInfoSync();
    const screenHeight = systemInfo.safeArea.height; // 获取屏幕可用高度
    setTimeout(() => {
      // 获取底部footer的高度
      getFooterHeight()
      getheaderHeight()
    }, 1000);

    setupDefaultComponents()
  })
  // 页面重新出现后需要重新注册组件  避免被其他页面的layout中的公共组件把实例覆盖
  onShow(() => {
    setupDefaultComponents()
  })


  //将组件内部的属性或方法暴漏给父组件
  defineExpose({
    getRect
  })
</script>

<style lang="less" scoped>
  .page-container {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    box-sizing: border-box;

    .page-header {
      width: 100%;
    }

    .page-content {
      flex: 1;
      overflow-y: auto;
    }

    .page-footer {
      width: 100%;
      box-sizing: border-box;
      box-shadow: 0 -4rpx 16rpx 0 rgba(0, 0, 0, 0.05);
      position: fixed;
      left: 0;
      bottom: 0;
      z-index: 99;

      &.fixed {}
    }
  }

  .nav-bar__left,
  .nav-bar__right {
    width: 180rpx;
  }



  .title {
    display: flex;
    justify-content: center;
    align-items: center;
    color: #fff;
    flex: 1;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }

  .placeholder {
    width: 100%;
    height: 0rpx;
  }
</style>

二. 关键功能实现

1. 灵活的插槽设计

组件使用了 Vue 的插槽机制,提供了极大的灵活性:

  • 默认头部:使用 Navbar 组件,支持自定义标题

  • 内容区域:默认插槽用于放置页面主要内容

  • 底部区域:具名插槽 footer 允许自定义底部内容

2. 设备适配解决方案

状态栏与安全区域适配
<!-- 状态栏占位 -->
<fui-status-bar></fui-status-bar>

<!-- 底部安全区域 -->
<fui-safe-area v-if="!isTabbarPage && showFixedSafeArea"></fui-safe-area>

组件自动检测是否为 tabbar 页面,避免重复添加安全区域

动态高度计算
// 获取底部高度
const getFooterHeight = async () => {
  const data = await getRect('footer')
  bottomHeight.value = data?.height ?? 0
}

// 获取元素尺寸
const getRect = (module: 'header' | 'footer' | 'content') => {
  return new Promise<UniApp.NodeInfo>((resolve) => {
    const query = uni.createSelectorQuery().in(instance)
    query.select(`.page-${module}`).boundingClientRect(resolve).exec()
  })
}

3.全局组件管理

组件集成了常用的全局 UI 组件,并统一管理它们的实例:

// 注册全局组件
const setupDefaultComponents = () => {
  const app = getApp()
  app.globalData!.landscape = landscape.value
  app.globalData!.dialog = dialog.value
  app.globalData!.toast = toast.value
}

// 确保页面显示时重新注册
onShow(() => {
  setupDefaultComponents()
})

样式实现要点

.page-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  
  .page-content {
    flex: 1;  // 关键:使内容区域填充剩余空间
    overflow-y: auto;
  }
  
  .page-footer {
    position: fixed;  // 固定底部
    left: 0;
    bottom: 0;
  }
  
  .placeholder {
    height: 0rpx;  // 底部占位,防止内容被遮挡
  }
}

总结

这个布局组件解决了移动端开发中的常见问题:

  • 不同设备的适配

  • 滚动内容与固定区域的协调

  • 全局组件的统一管理

  • 灵活的页面结构定制

使用示例
 

<PageLayout >
  <!-- 主要内容 -->
  <view>...</view>
  
  <!-- 自定义底部 -->
  <template #footer>
    <view class="custom-footer">...</view>
  </template>
</PageLayout>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值