331 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
		
		
			
		
	
	
			331 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
|  | <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 || item.isDot" | |||
|  | 						: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 { | |||
|  | 		props: { | |||
|  | 			// 显示与否
 | |||
|  | 			show: { | |||
|  | 				type: Boolean, | |||
|  | 				default: true | |||
|  | 			}, | |||
|  | 			// 通过v-model绑定current值
 | |||
|  | 			value: { | |||
|  | 				type: [String, Number], | |||
|  | 				default: 0 | |||
|  | 			}, | |||
|  | 			// 整个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: { | |||
|  | 			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.value ? 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.value ? 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) { | |||
|  | 					uni.switchTab({ | |||
|  | 						url: this.list[index].pagePath | |||
|  | 					}) | |||
|  | 				} else { | |||
|  | 					// 如果配置了papgePath属性,将不会双向绑定v-model传入的value值
 | |||
|  | 					// 因为这个模式下,不再需要v-model绑定的value值了,而是通过getCurrentPages()适配
 | |||
|  | 					this.$emit('input', 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> |