乐于分享
好东西不私藏

uni-app 标签栏组件 v-tabs 全攻略:14 个示例带你从入门到精通

uni-app 标签栏组件 v-tabs 全攻略:14 个示例带你从入门到精通

在移动端 App 和小程序中,标签栏(Tabs) 是最常见的交互组件之一。无论是新闻资讯的分类筛选、电商的商品分类、还是社交平台的频道切换,都离不开它。今天,我们通过 14 个由浅入深的实战示例,彻底掌握 uni-app 中 v-tabs 组件的使用。


一、为什么选择 v-tabs?

在 uni-app 的跨平台开发中(H5、微信小程序、App),标签栏组件需要兼顾以下几点:

  • 多端兼容
    :在 H5、小程序、App 上表现一致
  • 灵活配置
    :支持滚动/平铺、胶囊/下划线、自定义配色等多种形态
  • 简单易用
    :开箱即用,API 简洁

v-tabs 正是这样一款轻量级标签栏组件,它支持 18 个配置属性 和 1 个事件,足以覆盖绝大多数业务场景。


二、快速上手

2.1 安装组件

将 v-tabs.vue 放入项目的 components/v-tabs/ 目录下。uni-app 的 easycom 规则会自动注册,无需手动 import。

2.2 最小可用代码

<template> <view> <v-tabsv-model=”current”:tabs=”tabs” @change=”changeTab”></v-tabs> </view></template><script>export default { data() { return { current0, tabs: ['推荐''热门''最新''视频'] } }, methods: { changeTab(index) { console.log('当前选中:', index) } }}</script>

仅仅 3 个属性就能跑起来:v-model 双向绑定选中索引,:tabs 传入标签数组,@change 监听切换事件。


三、14 个实战示例

示例 01:基本用法(可滚动标签)

最基础的使用方式。默认 scroll=true,当标签数量较多超出屏幕宽度时,可横向滚动并自动将选中项滚动到屏幕中央。

<v-tabs v-model=”current” :tabs=”tabs” @change=”changeTab”></v-tabs>
data() { return { current0, tabs: ['推荐''热门''最新''视频''图片''音乐''科技''体育''娱乐''财经'] }}

适用场景:新闻资讯 App 的分类频道、音乐 App 的歌单分类。


示例 02:平铺标签(scroll=false)

设置 :scroll="false" 后,标签会均匀分布在整个容器宽度内,不再可滚动。适合标签数量较少(3-5个)的场景。

<v-tabs v-model=”current” :tabs=”tabs” :scroll=”false” @change=”changeTab”></v-tabs>
tabs: ['全部''待付款''待发货''已完成']

适用场景:订单状态筛选、个人中心 Tab 切换。


示例 03:胶囊样式(pills=true)

开启 :pills="true" 后,选中项以背景色块高亮,替代底部下划线。搭配 activeColor="#fff" 让文字在色块上显示为白色。

<v-tabs v-model=”current” :tabs=”tabs” :pills=”true” line-height=”0” activeColor=”#fff” pillsColor=”#2979ff” pillsBorderRadius=”30rpx”></v-tabs>

三种胶囊配色方案:

方案
pillsColor
pillsBorderRadius
场景
经典蓝
#2979ff
30rpx
内容导航
活力橙
#ff6b35
35rpx
电商分类
深邃紫
#533483
40rpx
深色主题

注意:胶囊模式下建议将 line-height="0" 隐藏底部下划线。


示例 04:自定义颜色

通过 bgColorcoloractiveColorlineColor 四个属性自由定制配色方案。

暗色主题:

<v-tabs v-model=”current” :tabs=”tabs” bgColor=”#1a1a2e” color=”#aaa” activeColor=”#e94560” lineColor=”#e94560” lineHeight=”6rpx”></v-tabs>

渐变背景:

<v-tabs v-model=”current” :tabs=”tabs” bgColor=”linear-gradient(90deg, #667eea, #764ba2)” color=”rgba(255,255,255,0.6)” activeColor=”#fff” lineColor=”#fff”></v-tabs>

小技巧bgColor 不仅支持纯色,还支持 linear-gradient 渐变语法!


示例 05:字体大小与加粗

<v-tabs v-model=”current” :tabs=”tabs” fontSize=”26rpx” activeFontSize=”34rpx” :bold=”true”></v-tabs>
  • fontSize
    :默认文字大小(28rpx)
  • activeFontSize
    :选中时文字大小(32rpx),制造大小差异可增加层次感
  • bold
    :选中项是否加粗,默认 true

示例 06:高度与下划线样式

下划线的样式可以通过多个属性精细控制:

<v-tabs v-model=”current” :tabs=”tabs” height=”90rpx” lineHeight=”6rpx” lineColor=”#e94560” :lineScale=”0.3” lineRadius=”6rpx”></v-tabs>
属性
作用
默认值
height
标签栏整体高度
70rpx
lineHeight
下划线高度
10rpx
lineColor
下划线颜色
#2979ff
lineScale
下划线宽度 = 文字宽度 * lineScale
0.5
lineRadius
下划线圆角
10rpx

设计建议lineScale 设为 0.3~0.5 时下划线精致短小,设为 1.0 时与文字等宽,风格更大胆。


示例 07:对象数组(field 属性)

实际业务中,标签数据往往不只是字符串,而是包含 id、name 等字段的对象。这时通过 field 指定要显示的字段名即可。

<v-tabs v-model=”current” :tabs=”categoryTabs” field=”name” :scroll=”false” activeColor=”#16813d” lineColor=”#16813d”></v-tabs>
data() { return { current: 0, categoryTabs: [ { id: 1, name: '水果', count: 12 }, { id: 2, name: '蔬菜', count: 8 }, { id: 3, name: '肉类', count: 15 }, { id: 4, name: '零食', count: 20 } ] }}

三种典型对象数组场景:

// 商品分类 —— field=”name”[{ id1name'水果'count12 }]// 城市选择 —— field=”label”[{ code'bj'label'北京' }]// 状态筛选 —— field=”title”[{ value'all'title'全部' }]

核心价值:使用 field 属性后,不再需要将对象数组手动映射为字符串数组,代码更简洁。


示例 08:固定在顶部(fixed=true)

<v-tabs v-model=”current” :tabs=”tabs” :fixed=”true”></v-tabs>

设置 :fixed="true" 后,标签栏会固定在页面顶部,不随页面内容滚动。组件内部会自动生成一个占位元素,防止内容被遮挡。

适用场景:长列表页面(新闻、商品列表),标签栏始终可见,方便用户随时切换分类。


示例 09:自定义 padding

<v-tabs v-model=”current” :tabs=”tabs” padding=”0 30rpx paddingItem=”0 30rpx></v-tabs>
  • padding
    :整个标签栏容器的内边距
  • paddingItem
    :每个标签项的内边距(默认 0 22rpx

增大 paddingItem 可以让标签之间更宽松,适合大屏或标签文字较长的场景。


示例 10:无动画模式

<v-tabs v-model=”current” :tabs=”tabs” :lineAnimation=”false”></v-tabs>

设置 :lineAnimation="false" 后,切换标签时下划线(或胶囊)会瞬间跳转,没有过渡动画。适合追求极简交互或性能敏感的场景。


示例 11:胶囊 + 渐变背景

将胶囊模式与渐变背景结合,打造更有视觉冲击力的标签栏:

<v-tabs v-model=”current” :tabs=”tabs” :pills=”true” :scroll=”false” line-height=”0” activeColor=”#fff” bgColor=”linear-gradient(90deg, #667eea, #764ba2)” pillsColor=”rgba(255,255,255,0.25)” pillsBorderRadius=”35rpx”></v-tabs>

设计灵感:半透明白色胶囊叠加在紫色渐变上,呈现出磨砂玻璃质感,适合潮流、社交类 App。


示例 12:动态修改 tabs

组件内部监听了 tabs 的变化,支持动态增删标签项:

// 添加标签this.tabs.push('新标签')// 移除末尾标签this.tabs.pop()if (this.current >= this.tabs.length) { this.current = this.tabs.length - 1 // 手动修正索引}

模拟异步加载:

// 页面加载时 tabs 为空数组data() { return { asyncTabs: [] }},// 异步请求完成后赋值loadData() { setTimeout(() => { this.asyncTabs = ['手机''电脑''平板''耳机''手表'] }, 500)}

关键点:移除标签后,一定要检查并修正 current 索引,防止越界报错。


示例 13:联动内容切换(最常用场景)

这是最贴近实际业务的用法——标签切换联动下方内容区域:

<v-tabs v-model=”current” :tabs=”tabs” :scroll=”false” activeColor=”#2979ff” lineColor=”#2979ff” :bold=”true” @change=”changeTab”></v-tabs><!-- 通过 v-if 切换内容面板 --><viewv-if=”current === 0”class=”tab-panel”> <viewv-for=”iteminrecommendList” :key=”item.id”> {{ item.title }} </view></view><viewv-if=”current === 1”class=”tab-panel”> <viewv-for=”iteminhotList” :key=”item.id”> {{ item.title }} </view></view>
data() { return { current: 0, tabs: ['推荐''热门''最新''视频'], recommendList: [ { title: '精选好文推荐', desc: '每日精选优质内容' }, { title: '编程入门指南', desc: '从零开始学习编程' } ], hotList: [ { title: '全网热议话题', desc: '本周最受关注的热门话题' }, { title: '技术突破新闻', desc: 'AI领域最新重大突破' } ] }},methods: { changeTab(index) { // 可在此处发起接口请求,实现按需加载 // this.loadData(this.tabs[index]) }}

进阶优化:在 changeTab 事件中根据标签发起 API 请求,避免一次性加载所有数据。


示例 14:外部控制选中项

通过外部修改 v-model 绑定的值,实现程序化切换。组件内部 watch 了 value,外部赋值会自动触发重新计算。

按钮控制:

<v-tabsv-model=”current”:tabs=”tabs”></v-tabs><button @click=”current = 0”>第1项</button><button @click=”current = 2”>第3项</button><button @click=”current = 4”>第5项</button>

上一项/下一项导航:

prev() { if (this.current > 0this.current--},next() { if (this.current < this.tabs.length - 1this.current++}

自动轮播:

toggleAutoPlay() { if (this.autoPlaying) { clearInterval(this.autoTimer) } else { this.autoTimer = setInterval(() => { this.current = (this.current + 1) % this.tabs.length }, 1500) } this.autoPlaying = !this.autoPlaying}

注意:页面销毁时一定要 clearInterval 清除定时器,避免内存泄漏。


四、完整属性速查表

属性
类型
默认值
说明
value
Number
0
选中的下标(v-model)
tabs
Array
[]
标签列表,支持字符串数组和对象数组
bgColor
String
'#fff'
背景颜色,支持渐变色
color
String
'#333'
默认(未选中)文字颜色
activeColor
String
'#2979ff'
选中文字颜色
fontSize
String
'28rpx'
默认文字大小
activeFontSize
String
'32rpx'
选中时文字大小
bold
Boolean
true
选中文字是否加粗
scroll
Boolean
true
是否可滚动
height
String
'70rpx'
标签栏高度
lineColor
String
'#2979ff'
下划线颜色
lineHeight
String
'10rpx'
下划线高度
lineScale
Number
0.5
下划线宽度缩放比例
lineRadius
String
'10rpx'
下划线圆角
pills
Boolean
false
是否胶囊样式
pillsColor
String
'#2979ff'
胶囊背景色
pillsBorderRadius
String
'10rpx'
胶囊圆角大小
field
String
''
对象数组时显示的键名
fixed
Boolean
false
是否固定在顶部
padding
String
'0'
容器内边距
paddingItem
String
'0 22rpx'
标签项内边距
lineAnimation
Boolean
true
下划线/胶囊是否有过渡动画

事件:

事件
参数
说明
@change
index(当前选中项索引)
切换标签时触发

五、踩坑指南

5.1 小程序模板中避免直接使用特殊字符

在微信小程序中,模板文本里的 @: 等字符可能被 WXML 编译器误解析为指令语法。解决方案:将包含特殊字符的代码示例存储为 data 属性,通过数据绑定显示。

<!-- 错误写法:直接写在模板中可能报错 --><text>@change</text><!-- 正确写法:通过数据绑定 --><text>{{ propChange }}</text>

5.2 动态删除标签要修正索引

removeTab() { this.tabs.pop() // 必须检查!否则删除最后一个标签后 current 可能越界 if (this.current >= this.tabs.length) { this.current = this.tabs.length - 1 }}

5.3 胶囊模式记得隐藏下划线

<!-- 胶囊模式下隐藏下划线 --><v-tabs:pills=”true”line-height=”0”></v-tabs>

5.4 fixed 模式的占位问题

使用 :fixed="true" 时,组件内部会自动生成占位元素。但如果标签栏高度被自定义 height 修改过,注意检查内容是否被遮挡。


六、总结

通过 14 个示例,我们覆盖了 v-tabs 组件的全部核心功能:

  • 基础能力
    :滚动/平铺、双向绑定、切换事件
  • 视觉定制
    :颜色、字体、下划线、胶囊、渐变
  • 数据灵活
    :字符串数组、对象数组(field)、动态增删
  • 交互进阶
    :固定定位、外部控制、联动内容、自动轮播

v-tabs 的设计理念是"配置即所得"——通过属性组合就能实现各种效果,不需要写大量自定义 CSS。对于 uni-app 跨平台项目来说,是一个开箱即用的标签栏解决方案。

v-tabs.vue

<template>  <view:id="elId"class="v-tabs">    <scroll-view      id="scrollContainer"      :scroll-x="scroll"      :scroll-left="scroll ? scrollLeft : 0"      :scroll-with-animation="scroll"      :style="{ position: fixed ? 'fixed' : 'relative', zIndex: 1993 }"    >      <view        class="v-tabs__container"        :style="{          display: scroll ? 'inline-flex' : 'flex',          whiteSpace: scroll ? 'nowrap' : 'normal',          background: bgColor,          height,          padding        }"      >        <view          class="v-tabs__container-item"          v-for="(v, i) in tabs"          :key="i"          :style="{            color: current == i ? activeColor : color,            fontSize: current == i ? fontSize : fontSize,            fontWeight: bold && current == i ? 'bold' : '',            justifyContent: !scroll ? 'center' : '',            flex: scroll ? '' : 1,            padding: paddingItem          }"          @click="change(i)"        >          {{ field ? v[field] : v }}        </view>        <view          v-if="!pills"          :class="['v-tabs__container-line', {animation: lineAnimation}]"          :style="{            background: lineColor,            width: lineWidth + 'px',            height: lineHeight,            borderRadius: lineRadius,            left: lineLeft + 'px',            transform: `translateX(-${lineWidth / 2}px)`          }"        ></view>        <view          v-else          :class="['v-tabs__container-pills', {animation: lineAnimation}]"          :style="{            background: pillsColor,            borderRadius: pillsBorderRadius,            left: pillsLeft + 'px',            width: currentWidth + 'px',            height          }"        ></view>      </view>    </scroll-view>    <view      class="v-tabs__placeholder"      :style="{        height: fixed ? height : '0',        padding      }"    ></view>  </view></template><script>/** * v-tabs * @property {Number} value 选中的下标 * @property {Array} tabs tabs 列表 * @property {String} bgColor = '#fff' 背景颜色 * @property {String} color = '#333' 默认颜色 * @property {String} activeColor = '#2979ff' 选中文字颜色 * @property {String} fontSize = '28rpx' 默认文字大小 * @property {String} activeFontSize = '28rpx' 选中文字大小 * @property {Boolean} bold = [true | false] 选中文字是否加粗 * @property {Boolean} scroll = [true | false] 是否滚动 * @property {String} height = '60rpx' tab 的高度 * @property {String} lineHeight = '10rpx' 下划线的高度 * @property {String} lineColor = '#2979ff' 下划线的颜色 * @property {Number} lineScale = 0.5 下划线的宽度缩放比例 * @property {String} lineRadius = '10rpx' 下划线圆角 * @property {Boolean} pills = [true | false] 是否胶囊样式 * @property {String} pillsColor = '#2979ff' 胶囊背景色 * @property {String} pillsBorderRadius = '10rpx' 胶囊圆角大小 * @property {String} field 如果是对象,显示的键名 * @property {Boolean} fixed = [true | false] 是否固定 * @property {String} paddingItem = '0 22rpx' 选项的边距 * @property {Boolean} lineAnimation = [true | false] 下划线是否有动画 * * @event {Function(current)} change 改变标签触发 */export default {  props: {    value: {      typeNumber,      default0    },    tabs: {      typeArray,      default () {        return []      }    },    bgColor: {      typeString,      default'#fff'    },    padding: {      typeString,      default'0'    },    color: {      typeString,      default'#333'    },    activeColor: {      typeString,      default'#2979ff'    },    fontSize: {      typeString,      default'28rpx'    },    activeFontSize: {      typeString,      default'32rpx'    },    bold: {      typeBoolean,      defaulttrue    },    scroll: {      typeBoolean,      defaulttrue    },    height: {      typeString,      default'70rpx'    },    lineColor: {      typeString,      default'#2979ff'    },    lineHeight: {      typeString,      default'10rpx'    },    lineScale: {      typeNumber,      default0.5    },    lineRadius: {      typeString,      default'10rpx'    },    pills: {      typeBoolean,      defaultfalse    },    pillsColor: {      typeString,      default'#2979ff'    },    pillsBorderRadius: {      typeString,      default'10rpx'    },    field: {      typeString,      default''    },    fixed: {      typeBoolean,      defaultfalse    },    paddingItem: {      typeString,      default'0 22rpx'    },    lineAnimation: {      typeBoolean,      defaulttrue    }  },  data () {    return {      elId'',      lineWidth30,      currentWidth0// 当前选项的宽度      lineLeft0// 滑块距离左侧的位置      pillsLeft0// 胶囊距离左侧的位置      scrollLeft0// 距离左边的位置      containerWidth0// 容器的宽度      current0 // 当前选中项    }  },  watch: {    value (newVal) {      this.current = newVal      this.$nextTick(() => {        this.getTabItemWidth()      })    },    current (newVal) {      this.$emit('input', newVal)    },    tabs (newVal) {      this.$nextTick(() => {        this.getTabItemWidth()      })    }  },  methods: {    // 产生随机字符串    randomString (len) {      len = len || 32      let $chars =        'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/      let maxPos = $chars.length      let pwd = ''      for (let i = 0; i < len; i++) {        pwd += $chars.charAt(Math.floor(Math.random() * maxPos))      }      return pwd    },    // 切换事件    change (index) {      if (this.current !== index) {        this.current = index        this.$emit('change', index)      }    },    // 获取左移动位置    getTabItemWidth () {      let query = uni        .createSelectorQuery()        // #ifndef MP-ALIPAY        .in(this)      // #endif      // 获取容器的宽度      query        .select(`#scrollContainer`)        .boundingClientRect((data) => {          if (!this.containerWidth && data) {            this.containerWidth = data.width          }        })        .exec()      // 获取所有的 tab-item 的宽度      query        .selectAll('.v-tabs__container-item')        .boundingClientRect((data) => {          if (!data) {            return          }          let lineLeft = 0          let currentWidth = 0          if (data) {            for (let i = 0; i < data.length; i++) {              if (i < this.current) {                lineLeft += data[i].width              } else if (i == this.current) {                currentWidth = data[i].width              } else {                break              }            }          }          // 当前滑块的宽度          this.currentWidth = currentWidth          // 缩放后的滑块宽度          this.lineWidth = currentWidth * this.lineScale * 1          // 滑块作移动的位置          this.lineLeft = lineLeft + currentWidth / 2          // 胶囊距离左侧的位置          this.pillsLeft = lineLeft          // 计算滚动的距离左侧的位置          if (this.scroll) {            this.scrollLeft = this.lineLeft - this.containerWidth / 2          }        })        .exec()    }  },  mounted () {    this.elId = 'xfjpeter_' + this.randomString()    this.current = this.value    this.$nextTick(() => {      this.getTabItemWidth()    })  }}</script><stylelang="scss"scoped>.v-tabs {  width100%;  box-sizing: border-box;  overflow: hidden;  ::-webkit-scrollbar {    display: none;  }  &__container {    min-width100%;    position: relative;    display: inline-flex;    align-items: center;    white-space: nowrap;    overflow: hidden;    &-item {      display: flex;      align-items: center;      height100%;      position: relative;      z-index10;      // padding0 11px;      transition: all 0.3s;      white-space: nowrap;    }    &-line {      position: absolute;      bottom0;    }    &-pills {      position: absolute;      z-index9;    }    &-line,    &-pills {      &.animation {        transition: all 0.3s linear;      }    }  }}</style>

觉得有用?点个「在看」收藏起来,下次做项目直接抄作业!