vue--实现Tabbar

最终实现的大致效果如下:

 

这里一样采用组件封装的方式,大致如下:

组件拆分:

  • MyHeader.vue – 复用之前的(上一个案例  vue--实现购物车 时里面封装的 头部组件)
  • MyTabBar.vue – 底部导航
  • MyTable.vue – 封装表格

三个页面

  • - MyGoodsList.vue – 商品页
  • - MyGoodsSearch.vue – 搜索页
  • - MyUserInfo.vue – 用户信息页

需要安装的第三方包:

npm install less less-loader@5.0.0 -D

npm install bootstrap --save   并在main.js 引入和全局属性

npm install axios --save    并在main.js 引入和全局属性

在 components 下分别新建 MyHeader.vue、MyTabBar.vue、MyTable.vue三个组件

然后再在 src 下新建 views 目录,并在该目录下分别新建 MyGoodsList.vue、MyGoodsSearch.vue、MyUserInfo.vue 三个组件页面

在 App.vue 里先引入注册和使用头部组件并且将头部的背景颜色、字体颜色、标题传递给 MyHeader.vue ,在 MyHeader.vue 通过 props 进行接收:

App.vue,

<MyHeader :background="'blue'" :fontColor="'white'" title="TabBar案例"></MyHeader>

MyHeader.vue,

<template>
  <div class="my-header"
       :style="{backgroundColor: background, color: color}"
  >{{title}}</div>
</template>

<script>
export default {
  props: {
    // 注意:外界在使用时,需要遵守变量名作为属性名,值的类型也要遵守
    background: String, // 外界传入此变量的类型为字符串
    color: {
      type: String, // 约束 color 值的类型
      default: '#fff', // 当前 color 的默认值(外界不传时使用默认值)
    },
    title: {
      type: String,
      required: true // 必须传入此变量的值
    }
  }
}
</script>

<style lang="less" scoped>
.my-header {
  height: 45px;
  line-height: 45px;
  text-align: center;
  background-color: #1d7bff;
  color: #fff;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 2;
}
</style>

这样头部也就完成了,效果如下:

 

完成底部封装,也就是 MyTabBar.vue 导航:

将 TabBar 需要的字体图标放入 assets 里,并在 main.js 里引入

main.js,

import './assets/fonts/iconfont.css' // 引入字体图标的 css

在 MyTabBar.vue 里完成基本的结构和样式

MyTabBar.vue,

<template>
  <div class="my-tab-bar">
  	<div class="tab-item">
      <!-- 图标 -->
      <span class="iconfont"></span>
      <!-- 文字 -->
      <span></span>
    </div>
  </div>
</template>

<script>
export default {
  
}
</script>

<style lang="less" scoped>
.my-tab-bar {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 50px;
  border-top: 1px solid #ccc;
  display: flex;
  justify-content: space-around;
  align-items: center;
  background-color: white;
  .tab-item {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
}
    
.current {
  color: #1d7bff;
}
</style>

为tabbar组件指定数据源 (数据源最少2个, 最多5个(validator) )

App.vue,

  data () {
    return {
      tabList: [
    {
        iconText: "icon-shangpinliebiao",
        text: "商品列表",
        componentName: "MyGoodsList"
    },
    {
        iconText: "icon-sousuo",
        text: "商品搜索",
        componentName: "MyGoodsSearch"
    },
    {
        iconText: "icon-user",
        text: "我的信息",
        componentName: "MyUserInfo"
    }
  ]
    }
  }

将 MyTabBar.vue 在 App.vue 里引入注册和使用,并通过 v-bind 传入底部导航的数据源

App.vue,

<MyTabBar :tabList="tabList"></MyTabBar>

在 MyTabBar.vue 里通过 props 接收传入的数据并循环进行展示

MyTabBar.vue,

  	<div class="tab-item" v-for="(obj, index) in tabList" :key="index">
      <!-- 图标 -->
      <span class="iconfont" :class="obj.iconText"></span>
      <!-- 文字 -->
      <span>{{obj.text}}</span>
    </div>


    props: {
    tabList: {
      type: Array,
      required: true,
      // 自定义校验规则
      validator(val){ // val 其实就是接到的数组
        if(val.length >= 2 && val.length <= 5){
          return true // 符合条件
        }else{
          console.error('数据源必须2~5项');
          return false
        }
      }
    }
  }

这样也就实现了:

 

实现点击底部导航完成点谁谁高亮显示效果:

给每个 TabBar 绑定点击事件, 并传入对应的索引

MyTabBar.vue,

<div class="tab-item" v-for="(obj, index) in tabList" :key="index" @click="btn(index)">

利用遍历的索引, 和点击保存的索引进行比较, 判断是否相等,若相同则给当前循环的标签设置动态class使其高亮显示,反之不高亮显示

  <div class="my-tab-bar">
  	<div class="tab-item" 
    v-for="(obj, index) in tabList" 
    :key="index" 
    @click="btn(index)"
    :class="{current: index === selIndex}"
    >

    
    data () {
    return {
      selIndex: 0  // 默认第一个高亮
    }
   },

    
    methods: {
    btn(index){
      this.selIndex = index // 点谁就把谁的索引值保存起来
     }
   }

  .current {
    color: #1d7bff;
  }

最后效果如下:

 

点击底部 TabBar 实现切换中间的内容组件

一个挂载点需要切换不同的组件显示需要用到动态组件或者是路由,这里以动态组件为例

将新建好的 MyGoodsList.vue、MyGoodsSearch.vue、MyUserInfo.vue 三个组件在 App.vue 里引入注册

App.vue ,

    <div class="main">
      <component :is="comName"></component>
    </div>


    data () {
    return {
      comName: 'MyGoodsList', // 默认显示的组件
    }
  },

利用动态组件 component 里的 :is 属性来指定需要显示的组件名,默认显示 商品列表页

实现点击 TabBar 时进行显示不同的页面组件,在 MyTabBar.vue 里当点击导航时同时也需要将obj传递到点击事件里,然后在点击事件中通过 $emit 将 obj 里面的 componentName 传递给 App.vue ,在App.vue 里通过 v-on 自定义事件进行接收

MyTabBar.vue,

  	<div class="tab-item" 
    v-for="(obj, index) in tabList" 
    :key="index" 
    @click="btn(index, obj)"
    :class="{current: index === selIndex}"
    >

     

    methods: {
    btn(index, obj){
      this.selIndex = index // 点谁就把谁的索引值保存起来
      // 把要切换的组件名传给父类
      this.$emit('changeCom', obj.componentName)
     }
    }

App.vue,

<MyTabBar :tabList="tabList" @changeCom="changeConFn"></MyTabBar>

  methods: {
    changeConFn(cName){
      // 将 MyTabBar.vue 里选出的组件名赋予给 is 属性的 comName 即可实现组件切换
      this.comName = cName
    }
  }


  .main {
    padding: 45px 0 51px 0;
   }

最后效果如下:

 

完成 商品列表 页面的数据填充:

封装MyTable.vue – 准备标签和样式

MyTable.vue,

<template>
  <table class="table table-bordered table-stripped">
    <!-- 表格标题区域 -->
    <thead>
      <tr>
        <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th>
      </tr>
    </thead>
    <!-- 表格主体区域 -->
    <tbody>
      <tr >
        <td>1</td>
        <td>商品</td>
        <td>998</td>
        <td>xxx</td>
        <td>xxx</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'MyTable'
}
</script>


<style scoped lang="less">
.my-goods-list {
  .badge {
    margin-right: 5px;
  }
}
</style>

在 MyGoodsList.vue 里引入注册和使用

MyGoodsList.vue,

<MyTable></MyTable>

大致效果如下:

在 main.js 里引入 bootstrap并配置axios的基地址

import 'bootstrap/dist/css/bootstrap.css'

import axios from 'axios'
// 配置基础地址
axios.defaults.baseURL = 'https://www.escook.cn'
// 将 axios 添加到 vue 原型上,这样就可在项目的任意地方使用
Vue.prototype.$axios = axios

在 MyGoodsList.vue 里请求数据并处理,然后在 data 里定义一个变量 list 保存请求到的数据,并通过 父传子 将保存好的数据传递给 MyTable.vue

MyGoodsList.vue,

    <MyTable :arr="list"></MyTable>
  
    data () {
    return {
      list: []
    }
  },
  created () {
    // 因为 axios 已经挂载到了全局上,因此无需引入,直接使用即可
    this.$axios({
      method: 'GET',
      url: '/api/goods'
    }).then(res => {
      // console.log(res);
      this.list = res.data.data
    }).catch(err => {
      console.log(err);
    })
  }

在 MyTable.vue 里通过 props 进行接收,并遍历数据

MyTable.vue,

  props: {
    arr: {
      type: Array,
      default: []
    }
  }


       <tbody>
      <tr v-for="obj in arr" :key="obj.id">
        <td>{{obj.id}}</td>
        <td>{{obj.goods_name}}</td>
        <td>{{obj.goods_price}}</td>
        <td>{{obj.tags}}</td>
        <td>
          <button class="btn btn-danger btn-sm">删除</button>
        </td>
      </tr>
    </tbody>

效果如下:

 

 实现允许用户自定义表格头和表格单元格内容:

之前在 MyTable.vue 里将表格头都写死了,这样不利于复用

 因此为了实现可以复用,我们可以将 tr里面的内容 换成 slot 插槽

 同理我们也应该将表格的主体区域 tr 里的 td 换成 slot 插槽

 这样可以最大程度上实现组件复用

然后在 MyGoodsList.vue 里通过 <template v-slot:>进行自定义

MyGoodsList.vue,

  <div>
    <MyTable :arr="list">
      <!-- v-slot: 等价于 # -->
      <template #header>
        <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th>
      </template>
      <template v-slot: = 'body'>
        <td>{{obj.id}}</td>
        <td>{{obj.goods_name}}</td>
        <td>{{obj.goods_price}}</td>
        <td>{{obj.tags}}</td>
        <td>
          <button class="btn btn-danger btn-sm">删除</button>
        </td>
      </template>
    </MyTable>
  </div>

由于在 tbody 里需要用到 MyTable.vue 里的数据,因此需要通过 作用域插槽 将数据传递过去

MyTable.vue,

<slot name="body" :row="obj"></slot>

在 MyGoodsList.vue 里的 template 标签里接收:

MyGoodsList.vue,

      <!-- scope的值:{row: obj} -->
      <template v-slot:body = 'scope'>
        <td>{{scope.row.id}}</td>
        <td>{{scope.row.goods_name}}</td>
        <td>{{scope.row.goods_price}}</td>
        <td>{{scope.row.tags}}</td>
        <td>
          <button class="btn btn-danger btn-sm">删除</button>
        </td>
      </template>

最后效果如下:

实现商品表格_tags铺设,使其标签列自定义显示:

 在插槽里传入 td 标签

然后自定义 span 标签进行循环展示并传入样式

MyGoodsList.vue,

      <template v-slot:body = 'scope'>
           ...
        <td>
          <span v-for="(val, index) in scope.row.tags" :key="index" class="badge badge-warning">
            {{val}}
          </span>
        </td>
        ...
      </template>

效果如下:

 

实现点击删除按钮删除数据:

给删除按钮绑定点击事件

利用作用域插槽绑定id值,传给删除方法, 删除MyGoodsList.vue里数组里数据

MyGoodsList.vue,

 <button class="btn btn-danger btn-sm" @click="delBtn(scope.row.id)">删除</button>

通过对应的 id 查找数组里的数据对应的索引实现删除

MyGoodsList.vue,

  methods: {
    delBtn(id){
      const index = this.list.findIndex(obj => {
        obj.id === id
      })
      this.list.splice(index, 1)
    }
  }

效果如下:

 

实现商品表格_添加tab:

需求,

  1. 点击Tab, 按钮消失, 输入框出现
  2. 输入框自动聚焦
  3. 失去焦点, 输入框消失, 按钮出
  4. 监测input回车, 无数据拦截
  5. 监测input取消, 清空数据
  6. 监测input回车, 有数据添加

准备静态Tab标签按钮 – 并绑定事件,当点击按钮时,按钮消失输入框出现,这里利用 v-if 实现

MyGoodsList.vue,

      <template v-slot:body = 'scope'>
       ...
        <td>
          <input
          class="tag-input form-control"
          style="width: 100px;"
          type="text"
          v-if="scope.row.inputVisible"
          />
          <button 
          v-else 
          style="display: block;" 
          class="btn btn-primary btn-sm add-tag"
          @click="scope.row.inputVisible = true"
          >+Tag</button>
         ...
        </td>
       ...
      </template>

在 main.js 里通过自定义指令, 让输入框自动聚焦

main.js,

// 全局指令
Vue.directive('gfocus', {
  inserted(el){
    // console.log(el);
    el.focus() // 触发标签的事件方法
  }
})

MyGoodsList.vue,

      <template v-slot:body = 'scope'>
       ...
        <td>
          <input
             ...
            v-gfocus
          />
         ...
        </td>
       ...
      </template>

监测失去焦点事件 – 给关联的对象属性设置 – 影响标签出现/隐藏   

MyGoodsList.vue,     

      <template v-slot:body = 'scope'>
       ...
        <td>
          <input
             ...
            @blur="scope.row.inputVisible = false"
          />
         ...
        </td>
       ...
      </template>

监测input的回车, 判断是否有值 – 给出提示 / 添加数据

监测input的取消, 清空数据

MyGoodsList.vue,    

      <template v-slot:body = 'scope'>
       ...
        <td>
          <input
             ...
            @keydown.enter="enterFn(scope.row)"
            v-model="scope.row.inputValue"
            @keydown.esc="scope.row.inputValue = ''"
          />
         ...
        </td>
       ...
      </template>



   methods: {
    // 回车
    enterFn(obj){
      if(obj.inputValue.trim().length === 0) {
        alert('请输入数据')
        return
      }
      // 将表单里的数据添加到数组里的tags里
      obj.tags.push(obj.inputValue)

      obj.inputValue = ''
    }
  },

效果如下:

 

本案例的最终效果和源码:

 

main.js,

import 'bootstrap/dist/css/bootstrap.css'
import './assets/fonts/iconfont.css' // 引入字体图标的 css

import axios from 'axios'
// 配置基础地址
axios.defaults.baseURL = 'https://www.escook.cn'
// 将 axios 添加到 vue 原型上,这样就可在项目的任意地方使用
Vue.prototype.$axios = axios

// 全局指令
Vue.directive('gfocus', {
  inserted(el){
    // console.log(el);
    el.focus() // 触发标签的事件方法
  }
})

App.vue,

<template>
  <div>
    <MyHeader :background="'blue'" :fontColor="'white'" title="TabBar案例"></MyHeader>
    <div class="main">
      <component :is="comName"></component>
    </div>
    <MyTabBar :tabList="tabList" @changeCom="changeConFn"></MyTabBar>
  </div>
</template>

<script>
import MyHeader from './components/MyHeader'
import MyTabBar from './components/MyTabBar'
import MyGoodsList from './views/MyGoodsList'
import MyGoodsSearch from './views/MyGoodsSearch'
import MyUserInfo from './views/MyUserInfo'
export default {
  components: {
    MyHeader,
    MyTabBar,
    MyGoodsList,
    MyGoodsSearch,
    MyUserInfo
  },
  data () {
    return {
      comName: 'MyGoodsList', // 默认显示的组件
      tabList: [
    {
        iconText: "icon-shangpinliebiao",
        text: "商品列表",
        componentName: "MyGoodsList"
    },
    {
        iconText: "icon-sousuo",
        text: "商品搜索",
        componentName: "MyGoodsSearch"
    },
    {
        iconText: "icon-user",
        text: "我的信息",
        componentName: "MyUserInfo"
    }
  ],
    }
  },
  methods: {
    changeConFn(cName){
      // 将 MyTabBar.vue 里选出的组件名赋予给 is 属性的 comName 即可实现组件切换
      this.comName = cName
    }
  }
}
</script>
<style scoped>
.main {
  padding: 45px 0 51px 0;
}
</style>

MyHeader.vue,

<template>
  <div class="my-header"
       :style="{backgroundColor: background, color: color}"
  >{{title}}</div>
</template>

<script>
export default {
  props: {
    // 注意:外界在使用时,需要遵守变量名作为属性名,值的类型也要遵守
    background: String, // 外界传入此变量的类型为字符串
    color: {
      type: String, // 约束 color 值的类型
      default: '#fff', // 当前 color 的默认值(外界不传时使用默认值)
    },
    title: {
      type: String,
      required: true // 必须传入此变量的值
    }
  }
}
</script>

<style lang="less" scoped>
.my-header {
  height: 45px;
  line-height: 45px;
  text-align: center;
  background-color: #1d7bff;
  color: #fff;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 2;
}
</style>

MyTable.vue,

<template>
  <table class="table table-bordered table-stripped">
    <!-- 表格标题区域 -->
    <thead>
      <tr>
        <!-- <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th> -->
        <slot name="header"></slot>
      </tr>
    </thead>
    <!-- 表格主体区域 -->
    <tbody>
      <tr v-for="obj in arr" :key="obj.id">
        <!-- <td>{{obj.id}}</td>
        <td>{{obj.goods_name}}</td>
        <td>{{obj.goods_price}}</td>
        <td>{{obj.tags}}</td>
        <td>
          <button class="btn btn-danger btn-sm">删除</button>
        </td> -->
        <slot name="body" :row="obj"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'MyTable',
  props: {
    arr: {
      type: Array,
      default: []
    }
  }
}
</script>


<style scoped lang="less">
.my-goods-list {
  .badge {
    margin-right: 5px;
  }
}
</style>

MyTabBar.vue,

<template>
  <div class="my-tab-bar">
  	<div class="tab-item" 
    v-for="(obj, index) in tabList" 
    :key="index" 
    @click="btn(index, obj)"
    :class="{current: index === selIndex}"
    >
      <!-- 图标 -->
      <span class="iconfont" :class="obj.iconText"></span>
      <!-- 文字 -->
      <span>{{obj.text}}</span>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    tabList: {
      type: Array,
      required: true,
      // 自定义校验规则
      validator(val){ // val 其实就是接到的数组
        if(val.length >= 2 && val.length <= 5){
          return true // 符合条件
        }else{
          console.error('数据源必须2~5项');
          return false
        }
      }
    }
  },
  data () {
    return {
      selIndex: 0  // 默认第一个高亮
    }
  },
  methods: {
    btn(index, obj){
      this.selIndex = index // 点谁就把谁的索引值保存起来
      // 把要切换的组件名传给父类
      this.$emit('changeCom', obj.componentName)
    }
  }
}
</script>

<style lang="less" scoped>
.my-tab-bar {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 50px;
  border-top: 1px solid #ccc;
  display: flex;
  justify-content: space-around;
  align-items: center;
  background-color: white;
  .tab-item {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
}
    
.current {
  color: #1d7bff;
}
</style>

MyGoodsList.vue,

<template>
  <div>
    <MyTable :arr="list">
      <!-- v-slot: 等价于 # -->
      <template #header>
        <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th>
      </template>
      <!-- scope的值:{row: obj} -->
      <template v-slot:body = 'scope'>
        <td>{{scope.row.id}}</td>
        <td>{{scope.row.goods_name}}</td>
        <td>{{scope.row.goods_price}}</td>
        <td>
          <input
          class="tag-input form-control"
          style="width: 100px;"
          type="text"
          v-if="scope.row.inputVisible"
          v-gfocus
          @blur="scope.row.inputVisible = false"
          @keydown.enter="enterFn(scope.row)"
          v-model="scope.row.inputValue"
          @keydown.esc="scope.row.inputValue = ''"
          />
          <button 
          v-else 
          style="display: block;" 
          class="btn btn-primary btn-sm add-tag"
          @click="scope.row.inputVisible = true"
          >+Tag</button>
          <span v-for="(val, index) in scope.row.tags" :key="index" class="badge badge-warning">
            {{val}}
          </span>
        </td>
        <td>
          <button class="btn btn-danger btn-sm" @click="delBtn(scope.row.id)">删除</button>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from '../components/MyTable'
export default {
  components: {
    MyTable
  },
  data () {
    return {
      list: []
    }
  },
  created () {
    // 因为 axios 已经挂载到了全局上,因此无需引入,直接使用即可
    this.$axios({
      method: 'GET',
      url: '/api/goods'
    }).then(res => {
      // console.log(res);
      this.list = res.data.data
    }).catch(err => {
      console.log(err);
    })
  },
  methods: {
    delBtn(id){
      const index = this.list.findIndex(obj => {
        obj.id === id
      })
      this.list.splice(index, 1)
    },
    // 回车
    enterFn(obj){
      if(obj.inputValue.trim().length === 0) {
        alert('请输入数据')
        return
      }
      // 将表单里的数据添加到数组里的tags里
      obj.tags.push(obj.inputValue)
      obj.inputValue = ''
    }
  },
  
}
</script>

<style>
</style>

MyGoodsSearch.vue,

<template>
  <div>商品搜素页</div>
</template>

<script>
export default {

}
</script>

<style>

</style>

MyUserInfo.vue,

<template>
  <div>个人中心</div>
</template>

<script>
export default {

}
</script>

<style>

</style>

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白小白从不日白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值