Skip to content

mytabbar.vue

jsx
<template>
	<u-tabbar v-model="pageData.current" :list="pageData.list" :mid-button="false" :border-top="false"
		active-color="rgb(19, 141, 179)" inactive-color="#fff" :hide-tab-bar="true"></u-tabbar>
</template>

<script setup>
	import {
		reactive,
		getCurrentInstance
	} from "vue";
	const {
		proxy
	} = getCurrentInstance();

	const props = defineProps({
		index: Number
	})
	const pageData = reactive({
		list: [{
				iconPath: proxy.Pub.getThemeImg('tabbar/icon_home.png'),
				selectedIconPath: proxy.Pub.getThemeImg('tabbar/icon_home_active.png'),
				text: '首页',
				customIcon: false,
				pagePath: '/pages/index/index',
			},
			{
				iconPath: proxy.Pub.getThemeImg('tabbar/icon_quotes.png'),
				selectedIconPath: proxy.Pub.getThemeImg('tabbar/icon_quotes_active.png'),
				text: '行情',
				customIcon: false,
				pagePath: '/pages/quotes/quotes',
			},
			{
				iconPath: proxy.Pub.getThemeImg('tabbar/icon_optional.png'),
				selectedIconPath: proxy.Pub.getThemeImg('tabbar/icon_optional_active.png'),
				text: '自选',
				customIcon: false,
				pagePath: '/pages/optional/optional',
			},
			{
				iconPath: proxy.Pub.getThemeImg('tabbar/icon_mine.png'),
				selectedIconPath: proxy.Pub.getThemeImg('tabbar/icon_mine_active.png'),
				text: '我的',
				customIcon: false,
				pagePath: '/pages/mine/mine',
			},
		],
		current: props.index || ''
	})
</script>

<style lang="scss">
	:deep(.u-tabbar__content__item__button) {
		top: 12rpx !important;
	}

	:deep(.u-tabbar__content__item__text) {
		bottom: 12rpx !important;
	}
</style>

tabbar.vue

jsx
<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?'48rpx':'0px'})`
			}"></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();
			// 页面栈中的最后一个即为项为当前页面,route属性为页面路径
			this.pageUrl = pages[pages.length - 1].route;
		},
		computed: {
			valueCom() {
				// #ifndef VUE3
				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();
			console.log(this.$u.addUnit(this.height));
		},
		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) {
					uni.switchTab({
						url: this.list[index].pagePath
					});
				} 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">
	@mixin vue-flex($direction: row) {
		/* #ifndef APP-NVUE */
		display: flex;
		flex-direction: $direction;
		/* #endif */
	}

	.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>