uniapp 小程序自定义底部导航
时间: 2025-07-20 15:25:49 浏览: 13
<think>我们正在讨论UniApp中自定义底部导航栏的实现。根据之前的引用和用户需求,用户希望在小程序中实现自定义底部导航栏。在UniApp中,默认的底部导航栏是原生组件,但有时我们需要自定义样式(如渐变色)或添加更多交互,这时就需要自定义底部导航栏。
实现方案通常有两种:
1. 完全自定义:隐藏原生导航栏,自己用view组件模拟导航栏,通过条件渲染切换页面。
2. 使用原生导航栏的扩展:通过配置pages.json中的tabBar,但这种方式只能实现一些基本的样式修改,对于复杂效果(如渐变色)可能不够灵活。
用户提到“导航栏设置渐变色”,所以我们需要采用完全自定义的方式。
步骤:
a. 在pages.json中,将需要自定义导航栏的页面设置`"navigationStyle": "custom"`,以隐藏原生导航栏(包括顶部和底部)。但注意,这样会隐藏整个页面的导航栏,包括顶部和底部。如果我们只想自定义底部导航栏,而保留顶部导航栏,那么我们需要单独处理:只隐藏原生底部导航栏,而顶部导航栏可以继续使用原生或也自定义。
然而,在UniApp中,底部导航栏(tabBar)是全局的,不能单独为某个页面隐藏。因此,我们通常的做法是:
- 全局隐藏原生tabBar(在pages.json的tabBar配置中不设置,或者不在tabBar的list中的页面就不会显示原生tabBar)
- 然后我们在需要自定义底部导航栏的页面(通常是所有tab页)都使用一个自定义的底部导航栏组件。
具体步骤:
1. 全局配置:在pages.json中,我们不在`tabBar`中配置,这样就不会有原生的底部导航栏。同时,我们每个页面的顶部导航栏可以按需设置(自定义或原生)。
2. 创建自定义底部导航栏组件:
- 在项目根目录下创建`components`文件夹(如果不存在),然后在其中创建自定义导航栏组件,例如`custom-tabbar`。
- 组件结构:使用`view`和`image`、`text`等组件构建导航栏,使用flex布局。
- 导航栏样式:可以设置背景渐变色、图标、文字等。
- 交互:通过`current`属性(或类似)指示当前激活的页面,点击切换时改变状态并跳转页面。
3. 在页面中使用:
- 在每一个tab页的底部引入该组件。注意,由于是自定义组件,需要先注册再使用。
- 通过变量控制当前激活的tab。
4. 页面切换:
- 使用`uni.switchTab`、`uni.reLaunch`或`uni.redirectTo`等API进行页面跳转。注意,tab页必须使用`switchTab`跳转,但自定义导航栏中,我们通常使用`switchTab`来切换tab页。
5. 适配不同设备:
- 底部导航栏需要适配安全区域(如iPhone X等有底部黑条的设备),可以使用`uni.getSystemInfoSync()`获取屏幕信息,然后给导航栏底部添加安全距离。
6. 状态管理:
- 由于多个页面共享同一个导航栏状态(当前激活的tab),我们可以使用全局变量(如Vuex)或者通过页面传参(在onShow中设置)来同步激活状态。
下面是一个简单的示例:
步骤1:创建组件`custom-tabbar.vue`
```vue
<template>
<view class="custom-tabbar" :style="{ paddingBottom: safeAreaInsets + 'px' }">
<view
v-for="(item, index) in list"
:key="index"
class="tabbar-item"
:class="{ active: current === index }"
@click="switchTab(item, index)"
>
<image :src="current === index ? item.selectedIconPath : item.iconPath" class="tab-icon"></image>
<text class="tab-text">{{ item.text }}</text>
</view>
</view>
</template>
<script>
export default {
name: "custom-tabbar",
props: {
current: {
type: Number,
default: 0
}
},
data() {
return {
safeAreaInsets: 0, // 底部安全区域高度
list: [
{
pagePath: "/pages/index/index",
text: "首页",
iconPath: "/static/tabbar/home.png",
selectedIconPath: "/static/tabbar/home_active.png"
},
{
pagePath: "/pages/category/index",
text: "分类",
iconPath: "/static/tabbar/category.png",
selectedIconPath: "/static/tabbar/category_active.png"
},
{
pagePath: "/pages/cart/index",
text: "购物车",
iconPath: "/static/tabbar/cart.png",
selectedIconPath: "/static/tabbar/cart_active.png"
},
{
pagePath: "/pages/user/index",
text: "我的",
iconPath: "/static/tabbar/user.png",
selectedIconPath: "/static/tabbar/user_active.png"
}
]
};
},
created() {
// 获取底部安全区域高度
const systemInfo = uni.getSystemInfoSync();
this.safeAreaInsets = systemInfo.safeAreaInsets.bottom;
},
methods: {
switchTab(item, index) {
if (this.current === index) return;
// 跳转到对应的tab页
uni.switchTab({
url: item.pagePath
});
}
}
};
</script>
<style lang="scss">
.custom-tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
background: linear-gradient(to right, #ff9569, #ff5263); // 渐变色背景
z-index: 999;
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 50px;
.tab-icon {
width: 24px;
height: 24px;
}
.tab-text {
font-size: 12px;
margin-top: 4px;
}
&.active {
.tab-text {
color: #fff;
font-weight: bold;
}
}
}
}
</style>
```
步骤2:在tab页中使用该组件,并传递当前激活的索引(current)。例如,在首页(pages/index/index.vue):
```vue
<template>
<view>
<!-- 页面内容 -->
<custom-tabbar :current="0"></custom-tabbar>
</view>
</template>
<script>
import CustomTabbar from '@/components/custom-tabbar.vue'
export default {
components: {
CustomTabbar
},
data() {
return {
// ...
}
}
}
</script>
```
注意:每个tab页都需要引入,并且传递正确的current值(0表示第一个,1表示第二个,以此类推)。
但是,这样每个页面都要写一次组件标签,并且要维护current值。我们可以通过一个全局的mixin或者使用Vuex来统一管理current。下面介绍一种简单的方法:
在每个tab页的onShow生命周期中,通过this.$emit或者直接修改组件props的方式改变current。但是,由于组件是各自独立的,我们可以使用全局状态(Vuex)来存储当前激活的tab索引。
这里为了简单,我们使用页面传参的方式:在每个tab页的onShow中设置一个全局变量(或者使用Vuex),然后在组件内使用这个全局变量。但更直接的方式是:在组件内部通过当前页面路径来判断当前激活的tab。
修改组件:在组件内部,根据当前页面路径自动计算current。
修改custom-tabbar组件的代码,去掉props:current,改为在组件内部计算current:
```vue
<script>
export default {
// ... 其他不变
data() {
return {
current: 0, // 内部状态
// ... 其他数据
};
},
mounted() {
this.updateCurrent();
},
methods: {
updateCurrent() {
// 获取当前页面路径
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = currentPage.route;
// 在微信小程序中,route是相对于项目根目录的,如:pages/index/index
// 我们需要和list中的pagePath匹配(注意pagePath前面有斜杠)
const currentIndex = this.list.findIndex(item =>
item.pagePath === `/${currentPath}` ||
item.pagePath === currentPath
);
if (currentIndex !== -1) {
this.current = currentIndex;
}
},
switchTab(item, index) {
if (this.current === index) return;
this.current = index; // 先更新当前激活项
uni.switchTab({
url: item.pagePath,
success: () => {
// 跳转成功后再更新一次,确保准确
this.updateCurrent();
}
});
}
}
};
</script>
```
注意:由于tab页切换后,页面栈会变化,我们可以通过监听页面显示(onShow)来更新current。但是,这个组件是固定在底部的,所以它不会销毁和重建。因此,我们可以在每个tab页的onShow事件中通知组件更新。然而,这样需要每个页面都做,我们可以使用全局事件总线或者Vuex。
这里我们采用另一种方式:在组件内部监听页面切换事件。UniApp提供了`uni.onAppRoute`方法(H5不支持,但小程序支持),可以在路由变化时触发。
修改组件:
```vue
<script>
export default {
// ...
mounted() {
this.updateCurrent();
// 监听路由变化
if (typeof uni.onAppRoute === 'function') {
this.routeCallback = (res) => {
if (res.type === 'switchTab') {
this.updateCurrent();
}
};
uni.onAppRoute(this.routeCallback);
}
},
beforeDestroy() {
if (this.routeCallback) {
uni.offAppRoute(this.routeCallback);
}
},
methods: {
updateCurrent() {
// 获取当前页面路径
const pages = getCurrentPages();
if (pages.length === 0) return;
const currentPage = pages[pages.length - 1];
const currentPath = currentPage.route;
const currentIndex = this.list.findIndex(item =>
item.pagePath === `/${currentPath}` ||
item.pagePath === currentPath
);
if (currentIndex !== -1) {
this.current = currentIndex;
}
},
// ...
}
};
</script>
```
这样,当切换tab时,路由变化会触发更新。
但是,注意:在H5端,`uni.onAppRoute`可能不支持,所以需要兼容。另外,在App端,可能也不支持。所以,更通用的做法是在每个tab页的onShow事件中触发组件的更新。我们可以通过事件总线(EventBus)或者Vuex来通知组件。
考虑到简单性,我们可以在每个tab页的onShow中,通过获取自定义组件的实例并调用其更新方法。但是,这样每个页面都要写,比较麻烦。
另一种思路:使用Vuex存储当前激活的tab索引,然后在组件中通过mapState获取。在每个tab页的onShow中,提交mutation更新状态。
这里我们使用Vuex:
步骤:
1. 在store中定义状态:
```js
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
tabbarIndex: 0 // 当前激活的tab索引
},
mutations: {
setTabbarIndex(state, index) {
state.tabbarIndex = index
}
}
})
```
2. 在每个tab页的onShow中提交mutation:
```js
// 在pages/index/index.vue中
export default {
onShow() {
this.$store.commit('setTabbarIndex', 0);
}
}
// 在pages/category/index.vue中
export default {
onShow() {
this.$store.commit('setTabbarIndex', 1);
}
}
// ... 以此类推
```
3. 修改custom-tabbar组件,使用Vuex中的状态:
```vue
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['tabbarIndex'])
},
data() {
return {
// 移除内部的current,使用Vuex的tabbarIndex
// ...
}
},
// 不再需要updateCurrent方法
methods: {
switchTab(item, index) {
if (this.tabbarIndex === index) return;
this.$store.commit('setTabbarIndex', index);
uni.switchTab({
url: item.pagePath
});
}
}
}
</script>
```
这种方式需要每个页面在onShow中提交,但是逻辑清晰,且兼容性好。
总结:根据项目复杂度选择方案。如果项目简单,可以使用基于页面路径自动检测的方式(注意兼容性);如果项目使用了Vuex,则推荐使用Vuex管理状态。
另外,关于渐变色:在组件的样式中,我们使用了线性渐变(linear-gradient),可以根据需要调整颜色和方向。
最后,注意自定义导航栏的位置固定(position: fixed; bottom:0;),并且要设置z-index确保在最上层。同时,页面内容要设置padding-bottom,避免被底部导航栏遮挡。可以在每个tab页的最外层view设置:
```css
.page-container {
padding-bottom: 60px; /* 根据导航栏高度调整 */
}
```
或者,在自定义导航栏组件中,我们使用了一个占位高度的view(在组件内部已经通过paddingBottom设置了安全区域,但是组件高度是50px,所以页面底部需要留出50px+安全区域的高度)。我们可以给组件一个固定高度,然后页面内容区域设置padding-bottom为这个高度。
但是,由于我们使用了固定定位,页面内容不会自动留出空间,所以我们需要在每个页面的根元素设置一个与导航栏高度(包括安全区域)相同的padding-bottom。例如,在组件的样式中,我们设置了:
```css
.custom-tabbar {
height: 50px;
padding-bottom: env(safe-area-inset-bottom); /* 使用CSS环境变量,注意兼容性 */
}
```
那么页面内容区域需要设置:
```css
/* 在tab页的样式 */
.tab-page {
padding-bottom: calc(50px + env(safe-area-inset-bottom)); /* 50px是导航栏高度 */
}
```
或者,我们可以不在每个页面设置,而是在组件内部增加一个占位的view(在页面内容最后),高度与导航栏相同,这样就不会遮挡内容。但是这样会多一个占位元素,且每个页面都要加。所以,我们可以在自定义导航栏组件上方添加一个等高的占位元素(非固定定位),但这样会占用页面内容高度。另一种做法是,在组件内部使用两个部分:一个固定定位的导航栏,一个等高的透明占位view(相对定位),这样页面内容就可以正常流动,不会被遮挡。
修改组件模板:
```vue
<template>
<view>
<!-- 占位区域,高度与导航栏相同(包括安全区域) -->
<view class="tabbar-placeholder" :style="{ height: totalHeight + 'px' }"></view>
<!-- 固定定位的导航栏 -->
<view class="custom-tabbar" :style="{ paddingBottom: safeAreaInsets + 'px' }">
<!-- 内容同上 -->
</view>
</view>
</template>
<script>
export default {
data() {
return {
// ...
totalHeight: 50 // 导航栏内容高度(不包括安全区域)
}
},
computed: {
// 总高度 = 内容高度 + 安全区域
totalBarHeight() {
return this.totalHeight + this.safeAreaInsets;
}
},
// ...
}
</script>
<style>
.custom-tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px; /* 内容高度 */
/* ... */
}
.tabbar-placeholder {
width: 100%;
/* 高度就是内容高度50px+安全区域,但注意这里我们只设置内容高度,因为安全区域在custom-tabbar的padding里,占位不需要安全区域,因为custom-tabbar的padding不占位? */
/* 实际上,占位区域只需要和custom-tabbar的内容高度一样,因为安全区域在下面,页面内容不会被遮挡到安全区域部分。所以占位高度就是50px */
height: 50px;
}
</style>
```
这样,在页面中引入组件后,页面内容就不会被遮挡了。
以上是自定义底部导航栏的完整方案。用户可以根据需要选择实现方式。
注意:在微信小程序中,自定义组件需要注册,并且在页面中使用时,要注意路径正确。
最后,关于引用[^1]和[^2]:
[^1]提到在单页面定义导航栏,包括设置背景颜色等,但这里我们讨论的是底部导航栏,所以主要参考了自定义的思路。
[^2]提到在页面中引入自定义导航栏组件,并通过tabIndex来设置当前激活项,这与我们的方案一致。
因此,我们实现了自定义底部导航栏,包括渐变背景色,并通过Vuex或页面路径来管理激活状态。
相关问题:
1. 在UniApp中,如何实现自定义顶部导航栏?
2. 自定义底部导航栏如何适配不同设备(特别是iPhone X等有安全区域的设备)?
3. 在UniApp中使用Vuex管理状态有哪些注意事项?</think>在 UniApp 中实现自定义底部导航栏,可以通过以下步骤完成:
### 实现步骤
1. **禁用原生导航栏**
```json
// pages.json
{
"tabBar": {
"custom": true, // 启用自定义导航栏
"list": [...] // 保留原有配置以便参考
}
}
```
2. **创建自定义组件**
```vue
<!-- components/custom-tabbar.vue -->
<template>
<view class="tabbar" :style="{ background: gradientBackground }">
<view
v-for="(item, index) in list"
:key="index"
class="tabbar-item"
:class="{ active: current === index }"
@click="switchTab(item, index)"
>
<image :src="current === index ? item.selectedIconPath : item.iconPath" />
<text>{{ item.text }}</text>
</view>
</view>
</template>
<script>
export default {
props: {
current: Number // 接收当前激活的索引
},
data() {
return {
gradientBackground: "linear-gradient(90deg, #FF9579, #FF5263)", // 渐变色
list: [
{
pagePath: "/pages/home/index",
text: "首页",
iconPath: "/static/tabbar/home.png",
selectedIconPath: "/static/tabbar/home-active.png"
},
// 其他导航项...
]
}
},
methods: {
switchTab(item, index) {
if (this.current === index) return;
uni.switchTab({ url: item.pagePath });
}
}
}
</script>
<style>
.tabbar {
position: fixed;
bottom: 0;
width: 100%;
height: 100rpx;
display: flex;
align-items: center;
box-shadow: 0 -2px 6px rgba(0,0,0,0.1);
}
.tabbar-item {
flex: 1;
text-align: center;
font-size: 24rpx;
}
.tabbar-item image {
width: 48rpx;
height: 48rpx;
margin-bottom: 4rpx;
}
</style>
```
3. **在页面中使用**
```vue
<!-- 页面中引入组件 -->
<template>
<view>
<!-- 页面内容 -->
<custom-tabbar :current="tabIndex" />
</view>
</template>
<script>
export default {
data() {
return {
tabIndex: 0 // 当前页面索引
}
}
}
</script>
```
4. **安全区域适配(iPhone等设备)**
```css
.tabbar {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
```
### 关键注意事项
1. **页面索引管理**
每个页面需设置对应的`tabIndex`(0表示第一个导航项,1表示第二个,以此类推)
2. **图标规范**
- 推荐尺寸:48x48 px
- 提供选中/未选中两种状态图标
- 使用绝对路径(/static/开头)
3. **跳转限制**
使用`uni.switchTab`而非`uni.navigateTo`,否则会显示原生导航栏[^1]
4. **全局状态(可选)**
复杂场景建议使用Vuex管理激活状态:
```js
// store中维护当前激活索引
state: { tabbarIndex: 0 }
```
### 渐变效果实现
通过CSS线性渐变实现:
```css
.tabbar {
background: linear-gradient(90deg, #FF9579, #FF5263);
}
```
支持方向/角度调整(如:`to right`、`45deg`)和多颜色节点[^1]
### 兼容性处理
1. **微信小程序**
需在`app.json`添加:
```json
"requiredBackgroundModes": ["audio"]
```
2. **H5端**
使用`position: fixed`时需确保父元素无`transform`样式
[^1]: uniapp开发微信小程序自定义顶部导航栏。重点来了,导航栏设置渐变色
[^2]: uniapp自定义底部导航栏。三、然后在首页引入<view-tabbar tabIndex=0></view-tabbar>,记住tabIndex为0
阅读全文
相关推荐

















