<template> <view v-if="show" class="u-tabbar" @touchmove.stop.prevent="() => {}"> <view class="u-tabbar__content safe-area-inset-bottom" :style="{ height: $u.addUnit(height), backgroundColor: bgColor }" :class="{ 'u-border-top': borderTop }" > <view class="u-tabbar__content__item" v-for="(item, index) in list" :key="index" :class="{ 'u-tabbar__content__circle': midButton && item.midButton }" @tap.stop="clickHandler(index)" :style="{ backgroundColor: bgColor }" > <view :class="[ midButton && item.midButton ? 'u-tabbar__content__circle__button' : 'u-tabbar__content__item__button' ]" > <u-icon :size="midButton && item.midButton ? midButtonSize : iconSize" :name="elIconPath(index)" img-mode="scaleToFill" :color="elColor(index)" :custom-prefix="item.customIcon ? 'custom-icon' : 'uicon'" ></u-icon> <u-badge :count="item.count" :is-dot="item.isDot" v-if="item.count" :offset="[-2, getOffsetRight(item.count, item.isDot)]" ></u-badge> </view> <view class="u-tabbar__content__item__text" :style="{ color: elColor(index) }" > <text class="u-line-1">{{ item.text }}</text> </view> </view> <view v-if="midButton" class="u-tabbar__content__circle__border" :class="{ 'u-border': borderTop }" :style="{ backgroundColor: bgColor, left: midButtonLeft }" ></view> </view> <!-- 这里加上一个48rpx的高度,是为了增高有凸起按钮时的防塌陷高度(也即按钮凸出来部分的高度) --> <view class="u-fixed-placeholder safe-area-inset-bottom" :style="{ height: `calc(${$u.addUnit(height)} + ${midButton ? 48 : 0}rpx)` }" ></view> </view> </template> <script> export default { emits: ["update:modelValue", "input", "change"], props: { // 通过v-model绑定current值 value: { type: [String, Number], default: 0 }, modelValue: { type: [String, Number], default: 0 }, // 显示与否 show: { type: Boolean, default: true }, // 整个tabbar的背景颜色 bgColor: { type: String, default: "#ffffff" }, // tabbar的高度,默认50px,单位任意,如果为数值,则为rpx单位 height: { type: [String, Number], default: "50px" }, // 非凸起图标的大小,单位任意,数值默认rpx iconSize: { type: [String, Number], default: 40 }, // 凸起的图标的大小,单位任意,数值默认rpx midButtonSize: { type: [String, Number], default: 90 }, // 激活时的演示,包括字体图标,提示文字等的演示 activeColor: { type: String, default: "#303133" }, // 未激活时的颜色 inactiveColor: { type: String, default: "#606266" }, // 是否显示中部的凸起按钮 midButton: { type: Boolean, default: false }, // 配置参数 list: { type: Array, default() { return []; } }, // 切换前的回调 beforeSwitch: { type: Function, default: null }, // 是否显示顶部的横线 borderTop: { type: Boolean, default: true }, // 是否隐藏原生tabbar hideTabBar: { type: Boolean, default: true } }, data() { return { // 由于安卓太菜了,通过css居中凸起按钮的外层元素有误差,故通过js计算将其居中 midButtonLeft: "50%", pageUrl: "" // 当前页面URL }; }, created() { // 是否隐藏原生tabbar if (this.hideTabBar) uni.hideTabBar(); // 获取引入了u-tabbar页面的路由地址,该地址没有路径前面的"/" let pages = getCurrentPages(); if (pages.length > 0) { // 页面栈中的最后一个即为项为当前页面,route属性为页面路径 this.pageUrl = pages[pages.length - 1].route; } }, computed: { valueCom() { // #ifdef VUE2 return this.value; // #endif // #ifdef VUE3 return this.modelValue; // #endif }, elIconPath() { return index => { // 历遍u-tabbar的每一项item时,判断是否传入了pagePath参数,如果传入了 // 和data中的pageUrl参数对比,如果相等,即可判断当前的item对应当前的tabbar页面,设置高亮图标 // 采用这个方法,可以无需使用v-model绑定的value值 let pagePath = this.list[index].pagePath; // 如果定义了pagePath属性,意味着使用系统自带tabbar方案,否则使用一个页面用几个组件模拟tabbar页面的方案 // 这两个方案对处理tabbar item的激活与否方式不一样 if (pagePath) { if (pagePath == this.pageUrl || pagePath == "/" + this.pageUrl) { return this.list[index].selectedIconPath; } else { return this.list[index].iconPath; } } else { // 普通方案中,索引等于v-model值时,即为激活项 return index == this.valueCom ? this.list[index].selectedIconPath : this.list[index].iconPath; } }; }, elColor() { return index => { // 判断方法同理于elIconPath let pagePath = this.list[index].pagePath; if (pagePath) { if (pagePath == this.pageUrl || pagePath == "/" + this.pageUrl) return this.activeColor; else return this.inactiveColor; } else { return index == this.valueCom ? this.activeColor : this.inactiveColor; } }; } }, mounted() { this.midButton && this.getMidButtonLeft(); }, methods: { async clickHandler(index) { if (this.beforeSwitch && typeof this.beforeSwitch === "function") { // 执行回调,同时传入索引当作参数 // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文 let beforeSwitch = this.beforeSwitch.bind(this.$u.$parent.call(this))(index); // 判断是否返回了promise if (!!beforeSwitch && typeof beforeSwitch.then === "function") { await beforeSwitch .then(res => { // promise返回成功, this.switchTab(index); }) .catch(err => {}); } else if (beforeSwitch === true) { // 如果返回true this.switchTab(index); } } else { this.switchTab(index); } }, // 切换tab switchTab(index) { // 发出事件和修改v-model绑定的值 this.$emit("change", index); // 如果有配置pagePath属性,使用uni.switchTab进行跳转 if (this.list[index].pagePath) { let url = this.list[index].pagePath; uni.switchTab({ url, fail: (err) => { if (err && err.errMsg && err.errMsg.indexOf("tabBar") > -1) { uni.navigateTo({ url }); } else { console.error(err); } } }); } else { // 如果配置了papgePath属性,将不会双向绑定v-model传入的value值 // 因为这个模式下,不再需要v-model绑定的value值了,而是通过getCurrentPages()适配 this.$emit("input", index); this.$emit("update:modelValue", index); } }, // 计算角标的right值 getOffsetRight(count, isDot) { // 点类型,count大于9(两位数),分别设置不同的right值,避免位置太挤 if (isDot) { return -20; } else if (count > 9) { return -40; } else { return -30; } }, // 获取凸起按钮外层元素的left值,让其水平居中 getMidButtonLeft() { let windowWidth = this.$u.sys().windowWidth; // 由于安卓中css计算left: 50%的结果不准确,故用js计算 this.midButtonLeft = windowWidth / 2 + "px"; } } }; </script> <style scoped lang="scss"> @import "../../libs/css/style.components.scss"; .u-fixed-placeholder { /* #ifndef APP-NVUE */ box-sizing: content-box; /* #endif */ } .u-tabbar { &__content { @include vue-flex; align-items: center; position: relative; position: fixed; bottom: 0; left: 0; width: 100%; z-index: 998; /* #ifndef APP-NVUE */ box-sizing: content-box; /* #endif */ &__circle__border { border-radius: 100%; width: 110rpx; height: 110rpx; top: -48rpx; position: absolute; z-index: 4; background-color: #ffffff; // 由于安卓的无能,导致只有3个tabbar item时,此css计算方式有误差 // 故使用js计算的形式来定位,此处不注释,是因为js计算有延后,避免出现位置闪动 left: 50%; transform: translateX(-50%); &:after { border-radius: 100px; } } &__item { flex: 1; justify-content: center; height: 100%; padding: 12rpx 0; @include vue-flex; flex-direction: column; align-items: center; position: relative; &__button { position: absolute; top: 14rpx; left: 50%; transform: translateX(-50%); } &__text { color: $u-content-color; font-size: 26rpx; line-height: 28rpx; position: absolute; bottom: 14rpx; left: 50%; transform: translateX(-50%); width: 100%; text-align: center; } } &__circle { position: relative; @include vue-flex; flex-direction: column; justify-content: space-between; z-index: 10; /* #ifndef APP-NVUE */ height: calc(100% - 1px); /* #endif */ &__button { width: 90rpx; height: 90rpx; border-radius: 100%; @include vue-flex; justify-content: center; align-items: center; position: absolute; background-color: #ffffff; top: -40rpx; left: 50%; z-index: 6; transform: translateX(-50%); } } } } </style>