基于Vue移动端私人UI库---底部导航栏tabbar

1,469 阅读2分钟

移动端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以及颜色的交互了。