移动端UI库--底部导航栏tabbar
前言
实现自己的一个ui库是我一直都想做的事情,本以为就是一些css样式美化,以及一些通用的点击事件.我以为写起来会十分的快捷,简单的组件比如说popup弹出层或者一些简单的dialog弹窗写起来很快,但是从前几天我就开始写这个tabbar组件,真是写的我“欲仙欲死”,但是最后还是显现大部分的功能。
github地址:github.com/suweishen
用到的知识点
- v-model父子组件双向绑定
- provide/inject 祖先--子孙组件传值
- $nextTick (dom更新完后执行)
- vue中的生命周期函数以及computed、watch的灵活应用
1.分析一下我们要实现的交互
> 1.组件需要位于页面的底部,并且可以适配例如IphoneX机型,要可控制显示安全区域
> 2.点击时会切换颜色以及icon的状态
> 3.父组件可以实时监控到目前处于tabbarItem的第几位
> 4.父组件可以传入数字,页面在第一次加载时默认显示第几个tabbarItem
最终实现结果如下
// 父组件中调用, 当前active为0
<v-tabbar v-model="active">
<v-tabbar-item icon="home" activeIcon="homeActive">首页</v-tabbar-item>
<v-tabbar-item icon="category" activeIcon="categoryActive">分类</v-tabbar-item>
<v-tabbar-item icon="search" activeIcon="searchActive">搜索</v-tabbar-item>
<v-tabbar-item icon="my" activeIcon="myActive">我的</v-tabbar-item>
</v-tabbar>
传入v-model="active",是为了让父子组件双向绑定,在子组件中做的操作,可以让父组件也能监控到,在这里我们就能实现第三点的需求了。
- tabbar.vue页面
<template> <div :class="[SafeArea, Common]" @click="clickTabbarItem($event)"> <slot></slot> </div> </template> <script> export default { name: 'v-tabbar', /* v-model和父组件进行双向绑定,父组件能监控到当前激活状态的tabbarItem是第几个 */ model: { prop: 'active', event: 'eventClick' }, props: { active: { type: Number, default: 0 }, // 是否显示安全区域 safeArea: { type: Boolean, default: true } }, data () { return { nodes: [], Active: this.active, tabbarItemId: '', } }, mounted () { this.nodes = document.querySelectorAll('.v-tabbar-item-wrapper') // 初始化显示icon的活跃状态 // dom操作完成后才能拿到tabbarItemId,所以需要用到nextTick this.$nextTick(() => { this.tabbarItemId = this.nodes[this.active].id }) }, provide () { // 整个页面提供给后代组件 return { tabbar: this } }, computed: { // 监控显示安全区域(默认显示) SafeArea: function () { if (this.safeArea) { let isIphone = /iphone/gi.test(window.navigator.userAgent) let windowW = window.screen.width let windowH = window.screen.height let pixelRatio = parseInt(window.devicePixelRatio) let isIPhoneX = isIphone && pixelRatio && pixelRatio === 3 && windowW === 375 && windowH === 812 let isIPhoneXSMax = isIphone && pixelRatio && pixelRatio === 3 && windowW === 414 && windowH === 896 let isIPhoneXR = isIphone && pixelRatio && pixelRatio === 2 && windowW === 414 && windowH === 896 if (isIPhoneX || isIPhoneXSMax || isIPhoneXR) { return 'fix-iphonex-bottom' } else { return '' } } else { return '' } }, Common: function () { return 'v-tabbar' }, }, methods: { // 切换tabbarItem区域颜色 changeColor () { if (this.Active < this.nodes.length) { this.nodes.forEach((item, index) => { if (index === this.Active) { item.style.color = '#2d8cf0' } else { item.style.color = '#808080' } }) } }, clickTabbarItem (e) { this.nodes.forEach((item, index) => { if (item.id === e.path[2].id) { this.Active = index this.tabbarItemId = item.id } }) let data = this.Active this.$emit('eventClick', data) } }, watch: { nodes: { handler(newVal) { if (newVal.length > 0) { this.changeColor() } }, immediate: true }, active: { handler(newVal) { if (newVal >= 0) { this.changeColor() } }, immediate: true } } } </script> <style lang="less" scoped> @import './index.less'; </style>
在这个tabbar.vue组件中我使用了provide/inject的语法,在provide()中把整个页面传给了后代组件(不仅仅是子组件,还可以是子子孙孙无穷尽也。。。) 这样子我们在后代组件中就可以通过inject来拿到tabbar.vue中的active的值了。
-
tabbarItem.vue组件
<template> <div class="v-tabbar-item-wrapper" :id="[tabbarItemId]"> <a class="v-tabbar-item"> <i :class="'v-icon-' + currentStatus" class="icon"></i> <span class="name"><slot></slot></span> </a> </div> </template> <script> export default { name: 'v-tabbar-item', // 接收祖先组件传过来的参数 inject: ['tabbar'], props: { // tabbar-item的图标 icon: { type: String }, // 点击时的icon activeIcon: { type: String }, }, mounted () { this.id = Math.floor(Math.random() * 10000) }, computed: { // 通过随机数来给每一个tabbarItem的id赋值,来完成不同的操作 tabbarItemId: function () { return 'v-tabbarItem-' + this.id } }, data () { return { change: false, currentStatus: this.icon, id: 0, } }, methods: { // 切换icon状态 changeIconActive () { let currentTabbarItemId = 'v-tabbarItem-' + this.id if (this.tabbar.tabbarItemId === currentTabbarItemId) { this.currentStatus = this.activeIcon } else { this.currentStatus = this.icon } } }, watch: { 'tabbar.tabbarItemId': { handler() { this.changeIconActive() }, immediate: true } } } </script> <style lang="less" scoped> @import './index.less'; @import '../../Icon/component/index.less'; </style>
在这里的难点就是因为我在tabbar.vue中是使用slot来填充数据的,所以就需要给每一个tabbarItem来生成一个唯一标识,所以我们就需要在生命周期mounted()函数中使用Math.random()来随机生成id这唯一表示
mounted () { this.id = Math.floor(Math.random() * 10000) }, computed: { // 通过随机数来给每一个tabbarItem的id赋值,来完成不同的操作 tabbarItemId: function () { return 'v-tabbarItem-' + this.id } },
这样子我们在tabbar.vue组件中就可以在点击的时候来遍历判断当前点击的是哪一个tabbarItem,于是就可以来实现我们的切换icon以及颜色的交互了。