在现代移动应用开发中,一个良好的页面布局组件可以显著提高开发效率,保持应用风格一致,并解决各种设备适配问题。本文将深入分析一个基于 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>
2281

被折叠的 条评论
为什么被折叠?



