封装一个底部导航

1,321 阅读1分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

TIP 👉 冤家宜解不宜结,各自回头看后头。明·冯梦龙《古今小说》

前言

在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。

底部导航

BottomNav组件属性

1. value
  • 选中值(即选中BottomNavPane的name值)
  • 值为字符串类型
  • 非必填默认为第一个BottomNavPane的name
2. lazy
  • 未显示的内容面板是否延迟渲染
  • 值为布尔类型
  • 默认为false
样式要求
  • 组件外面需要包裹可以相对定位的元素,增加样式:position: relative

BottomNavPane组件属性

1. name
  • 英文名称
  • 值为字符串类型
  • 必填
2. icon
  • 导航图标名称
  • 值为字符串类型
    • 值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
  • 必填
3. label
  • 导航图标下面显示的文字
  • 值为字符串类型
  • 必填
4. scroll
  • 是否有滚动条
  • 值为布尔类型
  • 默认值为:true

示例

<template>
  <div class="bottom-nav-wrap">
    <BottomNav v-model="curNav" :lazy="true">
      <BottomNavPane name="home" label="首页" icon="home">
        <h1>首页内容</h1>
      </BottomNavPane>
      <BottomNavPane name="oa" label="办公" icon="logo">
        <h1>办公内容</h1>
      </BottomNavPane>
      <BottomNavPane name="page2" label="我的" icon="user">
        <h1>个人中心</h1>
      </BottomNavPane>
    </BottomNav>
  </div>
</template>

<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'

export default {
  name: 'BottomNavDemo',
  components: {
    BottomNav,
    BottomNavPane
  },
  data () {
    return {
      curNav: ''
    }
  }
}
</script>

<style lang="scss" scoped>
.bottom-nav-wrap {
  position: absolute;
  top: $app-title-bar-height;
  bottom: 0;
  left: 0;
  right: 0;
}
</style>

BottomNav.vue

<template>
  <div class="bottom-nav">
    <div class="nav-pane-wrap">
      <slot></slot>
    </div>
    <div class="nav-list">
      <div class="nav-item"
           v-for="info in navInfos"
           :key="info.name"
           :class="{active: info.name === curValue}"
           @click="handleClickNav(info.name)">
        <Icon class="nav-icon" :name="info.icon"></Icon>
        <span class="nav-label">{{info.label}}</span>
      </div>
    </div>
  </div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
  name: 'BottomNav',
  props: {
    // 选中导航值(导航的英文名)
    value: String,
    // 未显示的内容面板是否延迟渲染
    lazy: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      // 组件实例的唯一ID
      id: generateUUID(),
      // 当前选中的导航值(导航的英文名)
      curValue: this.value,
      // 导航信息数组
      navInfos: [],
      // 导航面板vue实例数组
      panes: []
    }
  },
  watch: {
    value (val) {
      this.curValue = val
    },
    curValue (val) {
      this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
      this.$emit('cahnge', val)
    }
  },
  mounted () {
    this.calcPaneInstances()
  },
  beforeDestroy () {
    this.$eventBus.$off('CHANGE_NAV' + this.id)
  },
  methods: {
    // 计算导航面板实例信息
    calcPaneInstances () {
      if (this.$slots.default) {
        const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
          vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
        const panes = paneSlots.map(({ componentInstance }) => componentInstance)
        const navInfos = paneSlots.map(({ componentInstance }) => {
          // console.log(componentInstance.name, componentInstance)
          return {
            name: componentInstance.name,
            label: componentInstance.label,
            icon: componentInstance.icon
          }
        })
        this.navInfos = navInfos
        this.panes = panes
        if (!this.curValue) {
          if (navInfos.length > 0) {
            this.curValue = navInfos[0].name
          }
        } else {
          this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
        }
      }
    },
    // 导航点击事件处理方法
    handleClickNav (val) {
      this.curValue = val
    }
  }
}
</script>
<style lang="scss" scoped>
.bottom-nav {
  display: flex;
  flex-direction: column;
  height: 100%;
  .nav-pane-wrap {
    flex: 1;
  }
  .nav-list {
    flex: none;
    display: flex;
    height: 90px;
    background-color: #FFF;
    align-items: center;
    border-top: 1px solid $base-border-color;
    .nav-item {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      line-height: 1;
      text-align: center;
      color: #666;
      .nav-icon {
        font-size: 40px;/*yes*/
      }
      .nav-label {
        margin-top: 6px;
        font-size: 24px;/*yes*/
      }
      &.active {
        position: relative;
        color: $base-color;
      }
    }
  }
}
</style>

BottomNavPane.vue

<template>
  <div v-if="canInit" class="bottom-nav-pane" v-show="show">
    <Scroll v-if="scroll">
      <slot></slot>
    </Scroll>
    <slot v-else></slot>
  </div>
</template>
<script>
import Scroll from '@/components/base/scroll'

export default {
  name: 'BottomNavPane',
  components: {
    Scroll
  },
  props: {
    // 页签英文名称
    name: {
      type: String,
      required: true
    },
    // 页签显示的标签
    label: {
      type: String,
      required: true
    },
    // 图标名称
    icon: {
      type: String,
      required: true
    },
    // 是否有滚动条
    scroll: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {
      // 是否显示
      show: false,
      // 是否已经显示过
      hasShowed: false
    }
  },
  computed: {
    canInit () {
      return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
    }
  },
  created () {
    this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
      if (val === this.name) {
        this.show = true
        this.hasShowed = true
      } else {
        this.show = false
      }
    })
  }
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
  height: 100%;
  position: relative;
}
</style>
/**
 * 底部图标导航组件
 */
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下