小程序中,自定义导航栏组件是一个常见且实用的需求,它可以帮助开发者在不同平台上实现统一的导航栏样式,并且灵活适配各种屏幕尺寸和设备。
一、好处
- 用户体验:保持页面一致性,包括颜色、字体和布局等方面,提升用户体验。
- 适应性和灵活性:便于调整和优化。可以添加特定页面所需的功能按钮、搜索栏等。
- 提高开发效率:可以重复使用,减少代码冗余,提高开发效率。
- 功能扩展和定制化:功能扩展,如下拉菜单、动态标题等,满足不同页面和用户需求。
二、思路
- 隐藏自带的导航栏。
- 创建自定义导航栏组件。
- 导航栏组件使用。
三、实现
- pages.json 文件中设置
"navigationStyle": "custom"
。
pages.json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "\u200e",
"navigationStyle": "custom"
}
}
]
...
}
不需要自定义导航栏且不需要navigationBarTitleText
,在浏览器上会显示当前页面url,可以设置"navigationBarTitleText": "\u200e"
。
- 创建自定义导航栏组件
可以在uniApp项目的components
目录下新建一个custom-navbar
文件夹,并在其中创建custom-navbar.vue文件。小程序端根据状态栏、胶囊计算导航栏width、height。
vue
<template>
<view class="">
<view
class="navbar-box border-box"
:style="[navbarStyle]"
:class="[
{
'border-bottom': borderBottom,
'navbar-box-image': bgImage,
'navbar-fixed': isFixed,
},
]">
<view class="status-bar" :style="[{ height: statusBarHeight + 'px' }]"></view>
<view class="navbar-inner" :style="[navbarInnerStyle]">
<slot name="back">
<view
class="flex align-center navbar-inner-back-wrap"
:class="[{ 'navbar-inner-back-border': showLine }]">
<template v-if="isBack">
<view
class="navbar-inner-back text-nowrap"
:style="[
{
color: backIconColor,
fontSize: backIconSize + 'rpx',
},
]"
@tap="back()">
<text class="iconfont icon-xiangzuo"> </text>
<text v-if="backText" class="back-text line-1" :style="[backTextStyle]">
{{ backText }}
</text>
</view>
</template>
<view class="line" v-if="isBack && isHome" :style="[{ background: backIconColor }]"></view>
<template v-if="isHome">
<view
class="home-btn"
:style="[
{
color: backIconColor,
fontSize: backIconSize + 'rpx',
},
]"
@tap="home()">
<text class="iconfont icon-shouye"></text>
</view>
</template>
<view
class="line"
v-if="(isBack || isHome) && menu && menu.length"
:style="[{ background: backIconColor }]"></view>
<template v-if="menu && menu.length">
<view
class="home-btn menu-btn"
:style="[
{
color: backIconColor,
fontSize: backIconSize + 'rpx',
},
]"
@tap="showMenu()">
<text class="iconfont icon-caidan"></text>
<view v-show="show">
<view class="menu-mask"></view>
<view class="menu-wrap">
<view class="arrow"></view>
<view
v-for="(item, index) in menu"
:key="index"
class="item text-wrap"
:class="[
{
'item-disabled': item.disabled,
},
]"
@tap="menuClick(item, index)">
<text>{{ item.title }}</text>
</view>
</view>
</view>
</view>
</template>
</view>
</slot>
<slot name="content">
<view class="navbar-inner-content flex-1" v-if="title">
<view class="line-1" :style="[navTitleStyle]">
{{ title }}
</view>
</view>
</slot>
</view>
</view>
<!-- 解决fixed定位后导航栏塌陷的问题 -->
<view
class="navbar-placeholder"
v-if="isFixed && !immersive"
:style="[
{
width: '100%',
height: navHeight + statusBarHeight + 'px',
},
]"></view>
</view>
</template>
<script>
let { windowWidth, statusBarHeight } = uni.getSystemInfoSync();
let top = 0,
left = 12,
right = 12,
bottom = 0,
width = windowWidth,
height = 44;
// 小程序端自定义导航,根据状态栏,胶囊计算导航栏width、height、left、right、top、bottom
// #ifdef MP
let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
top = menuButtonInfo.top - (menuButtonInfo.top - statusBarHeight);
bottom =
top + (menuButtonInfo.top - statusBarHeight) * 2 + menuButtonInfo.height;
left = windowWidth - menuButtonInfo.right;
right = menuButtonInfo.width + left * 2;
width = windowWidth - left * 2 - menuButtonInfo.width - left;
height = bottom - top;
// #endif
export default {
props: {
// 是否显示返回
isBack: {
type: Boolean,
default: true,
},
// 返回箭头的颜色
backIconColor: {
type: String,
default: "#606266",
},
// 左边返回图标的大小,rpx
backIconSize: {
type: [String, Number],
default: "40",
},
// 返回的文字提示
backText: {
type: String,
default: "",
},
// 返回的文字的样式
backTextStyle: {
type: Object,
default() {
return {
color: "#606266",
};
},
},
// 是否显示回到主页
isHome: {
type: Boolean,
default: false,
},
// 导航栏标题
title: {
type: String,
default: "",
},
// 标题的颜色
titleColor: {
type: String,
default: "#606266",
},
// 标题字体是否加粗
titleBold: {
type: Boolean,
default: false,
},
// 标题的字体大小
titleSize: {
type: [String, Number],
default: 32,
},
// 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色
background: {
type: Object,
default() {
return {
background: "#ffffff",
};
},
},
// 导航栏是否固定在顶部
isFixed: {
type: Boolean,
default: true,
},
// 是否沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效
immersive: {
type: Boolean,
default: false,
},
// 是否显示导航栏的下边框
borderBottom: {
type: Boolean,
default: true,
},
zIndex: {
type: [String, Number],
default: "",
},
// 自定义返回
isCustomBack: {
type: Boolean,
default: false,
},
// 背景图
bgImage: {
type: String,
default: "",
},
// 菜单
menu: {
type: Array,
dafault: [],
},
},
emits: ["navbarHeight", "customBack", "homeClick", "menuClick"],
data() {
return {
navWidth: width,
navHeight: height,
navTop: top,
navLeft: left,
navRight: right,
statusBarHeight,
// 返回rect
backRect: {},
// menu菜单显隐
show: false,
};
},
computed: {
navbarStyle() {
let style = {};
style.zIndex = this.zIndex ? this.zIndex : "10";
if(!this.bgImage) {
Object.assign(style, this.background);
}
if (this.bgImage) {
style.backgroundImage = `url(${this.bgImage})`;
}
return style;
},
navbarInnerStyle() {
let style = {
height: this.navHeight + "px",
paddingLeft: this.navLeft + "px",
paddingRight: this.navRight + "px",
};
return style;
},
// 计算title的margin-left值,保证title居中显示
navTitleStyle() {
let style = {
color: this.titleColor,
fontSize: this.titleSize + "rpx",
fontWeight: this.titleBold ? "bold" : "normal",
};
let marginLeft = this.navRight - this.navLeft;
if (this.backRect.width) {
let backMarginLeft = uni.upx2px(20)
marginLeft = marginLeft - this.backRect.width - backMarginLeft;
}
style.marginLeft = marginLeft + "px";
return style;
},
showLine() {
return (
(this.isBack && this.isHome) || ((this.isBack || this.isHome) && this.menu && this.menu.length)
);
},
},
mounted() {
// 导航栏的高度,包含状态栏的高度
this.$emit("navbarHeight", this.navHeight + this.statusBarHeight);
if (this.isBack) {
this.$nextTick(async () => {
this.backRect = await this.getRect(".navbar-inner-back-wrap");
});
}
},
methods: {
// 查询节点信息
getRect(selector, all) {
return new Promise((resolve) => {
uni.createSelectorQuery().in(this)[all ? "selectAll" : "select"](selector)
.boundingClientRect((rect) => {
if (all && Array.isArray(rect) && rect.length) {
resolve(rect);
}
if (!all && rect) {
resolve(rect);
}
})
.exec();
});
},
// 返回
back() {
if (this.isCustomBack) {
this.$emit("customBack");
} else {
let pages = getCurrentPages();
pages.length > 1 && uni.navigateBack();
}
},
home() {
this.$emit("homeClick");
},
showMenu() {
this.show = !this.show;
},
// 菜单点击
menuClick(item, index) {
if (item.disabled) return;
this.$emit("menuClick", { ...item, index });
},
},
};
</script>
<style scoped lang="scss">
.border-box {
box-sizing: border-box;
}
.text-nowrap {
white-space: nowrap;
}
.text-wrap {
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
}
.line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.navbar-box {
&.border-bottom {
border-bottom: 1rpx solid #f0f0f0;
}
&.navbar-box-image {
background-position: 0 0;
background-repeat: no-repeat;
background-size: cover;
}
&.navbar-fixed {
position: fixed;
top: 0;
right: 0;
left: 0;
}
.navbar-inner {
display: flex;
align-items: center;
box-sizing: border-box;
.navbar-inner-back-wrap {
margin-right: 20rpx;
.navbar-inner-back {
.back-text {
margin-left: 10rpx;
}
}
&.navbar-inner-back-border {
padding: 0 20rpx;
border: 1rpx solid #f0f0f0;
border-radius: 40rpx;
box-sizing: border-box;
}
.line {
height: 32rpx;
width: 1rpx;
background-color: #f0f0f0;
margin: 0 20rpx;
}
.menu-btn {
position: relative;
}
}
.navbar-inner-content {
text-align: center;
}
}
}
$bgColor: #fff;
$borderColor: rgba(255, 255, 255, 0.4);
.menu-mask {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
.menu-wrap {
box-sizing: border-box;
position: absolute;
top: calc(100% + 24rpx);
left: 0;
background-color: $bgColor;
border: 1rpx solid $borderColor;
border-radius: 12rpx;
box-shadow: 0 4rpx 24rpx 0 rgba(0, 0, 0, 0.1);
z-index: 3;
padding: 8rpx 0;
.arrow {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12rpx;
filter: drop-shadow(0 4rpx 24rpx rgba(0, 0, 0, 0.03));
top: -12rpx;
left: 10%;
margin-right: 6rpx;
border-top-width: 0;
border-bottom-color: $borderColor;
&::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12rpx;
content: " ";
top: 2rpx;
margin-left: -12rpx;
border-top-width: 0;
border-bottom-color: $bgColor;
}
}
.item {
min-width: 124rpx;
max-width: 260rpx;
font-size: 28rpx;
padding: 10rpx 20rpx;
box-sizing: border-box;
color: #333;
&.item-disabled {
opacity: 0.4;
}
}
}
</style>
四、使用
使用示例
index.vue
<template>
<view>
<custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :background="{background: 'linear-gradient(144deg,#af40ff,#4f46e5)'}"></custom-navbar>
<view class="">
页面
</view>
</view>
</template>
<script>
import customNavbar from "@/components/custom-navbar/custom-navbar.vue";
export default {
component: {
customNavbar,
},
data() {
return {};
},
onLoad() {},
methods: {},
};
</script>
渐变背景
index.vue
<custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :background="{background: 'linear-gradient(144deg,#af40ff,#4f46e5)'}"></custom-navbar>
图片背景
index.vue
<custom-navbar backIconColor="#fff" :isHome="true" titleColor="#fff" title="首页" bgImage="https://img1.baidu.com/it/u=3915920165,3171018890&fm=253&fmt=auto&app=120&f=JPEG"></custom-navbar>
下拉菜单
index.vue
<custom-navbar backIconColor="#fff" titleColor="#fff" title="首页" :menu="[{title: '菜单一'}]" :background="{background: '#00AF57'}"></custom-navbar>
插槽,搜索框
index.vue
<custom-navbar>
<template v-slot:content>
<view class="search-box">
<text class="iconfont icon-sousuo ml-20" ></text>
<text>搜索</text>
</view>
</template>
</custom-navbar>
<style scoped lang="scss">
.search-box {
height: 80%;
width: 100%;
border-radius: 40rpx;
background-color: #f0f0f0;
padding: 0 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
}
.ml-20 {
margin-right: 20rpx;
}
</style>
五、效果
注:uniapp,vue3