在移动端 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 {current: 0,tabs: ['推荐', '热门', '最新', '视频']}},methods: {changeTab(index) {console.log('当前选中:', index)}}}</script>
仅仅 3 个属性就能跑起来:v-model 双向绑定选中索引,:tabs 传入标签数组,@change 监听切换事件。
三、14 个实战示例
示例 01:基本用法(可滚动标签)
最基础的使用方式。默认 scroll=true,当标签数量较多超出屏幕宽度时,可横向滚动并自动将选中项滚动到屏幕中央。
<v-tabsv-model=”current”:tabs=”tabs”@change=”changeTab”></v-tabs>
data() {return {current: 0,tabs: ['推荐', '热门', '最新', '视频', '图片', '音乐', '科技', '体育', '娱乐', '财经']}}
适用场景:新闻资讯 App 的分类频道、音乐 App 的歌单分类。

示例 02:平铺标签(scroll=false)
设置 :scroll="false" 后,标签会均匀分布在整个容器宽度内,不再可滚动。适合标签数量较少(3-5个)的场景。
<v-tabsv-model=”current”:tabs=”tabs”:scroll=”false”@change=”changeTab”></v-tabs>
tabs: ['全部', '待付款', '待发货', '已完成']适用场景:订单状态筛选、个人中心 Tab 切换。

示例 03:胶囊样式(pills=true)
开启 :pills="true" 后,选中项以背景色块高亮,替代底部下划线。搭配 activeColor="#fff" 让文字在色块上显示为白色。
<v-tabsv-model=”current”:tabs=”tabs”:pills=”true”line-height=”0”activeColor=”#fff”pillsColor=”#2979ff”pillsBorderRadius=”30rpx”></v-tabs>
三种胶囊配色方案:
注意:胶囊模式下建议将
line-height="0"隐藏底部下划线。

示例 04:自定义颜色
通过 bgColor、color、activeColor、lineColor 四个属性自由定制配色方案。
暗色主题:
<v-tabsv-model=”current”:tabs=”tabs”bgColor=”#1a1a2e”color=”#aaa”activeColor=”#e94560”lineColor=”#e94560”lineHeight=”6rpx”></v-tabs>
渐变背景:
<v-tabsv-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-tabsv-model=”current”:tabs=”tabs”fontSize=”26rpx”activeFontSize=”34rpx”:bold=”true”></v-tabs>
fontSize:默认文字大小(28rpx) activeFontSize:选中时文字大小(32rpx),制造大小差异可增加层次感 bold:选中项是否加粗,默认 true

示例 06:高度与下划线样式
下划线的样式可以通过多个属性精细控制:
<v-tabsv-model=”current”:tabs=”tabs”height=”90rpx”lineHeight=”6rpx”lineColor=”#e94560”:lineScale=”0.3”lineRadius=”6rpx”></v-tabs>
设计建议:
lineScale设为 0.3~0.5 时下划线精致短小,设为 1.0 时与文字等宽,风格更大胆。

示例 07:对象数组(field 属性)
实际业务中,标签数据往往不只是字符串,而是包含 id、name 等字段的对象。这时通过 field 指定要显示的字段名即可。
<v-tabsv-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”[{ id: 1, name: '水果', count: 12 }]// 城市选择 —— field=”label”[{ code: 'bj', label: '北京' }]// 状态筛选 —— field=”title”[{ value: 'all', title: '全部' }]
核心价值:使用
field属性后,不再需要将对象数组手动映射为字符串数组,代码更简洁。

示例 08:固定在顶部(fixed=true)
<v-tabsv-model=”current”:tabs=”tabs”:fixed=”true”></v-tabs>
设置 :fixed="true" 后,标签栏会固定在页面顶部,不随页面内容滚动。组件内部会自动生成一个占位元素,防止内容被遮挡。
适用场景:长列表页面(新闻、商品列表),标签栏始终可见,方便用户随时切换分类。

示例 09:自定义 padding
<v-tabsv-model=”current”:tabs=”tabs”padding=”0 30rpx”paddingItem=”0 30rpx”></v-tabs>
padding:整个标签栏容器的内边距 paddingItem:每个标签项的内边距(默认 0 22rpx)
增大 paddingItem 可以让标签之间更宽松,适合大屏或标签文字较长的场景。

示例 10:无动画模式
<v-tabsv-model=”current”:tabs=”tabs”:lineAnimation=”false”></v-tabs>
设置 :lineAnimation="false" 后,切换标签时下划线(或胶囊)会瞬间跳转,没有过渡动画。适合追求极简交互或性能敏感的场景。

示例 11:胶囊 + 渐变背景
将胶囊模式与渐变背景结合,打造更有视觉冲击力的标签栏:
<v-tabsv-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-tabsv-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 > 0) this.current--},next() {if (this.current < this.tabs.length - 1) this.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清除定时器,避免内存泄漏。

四、完整属性速查表
事件:
五、踩坑指南
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-viewid="scrollContainer":scroll-x="scroll":scroll-left="scroll ? scrollLeft : 0":scroll-with-animation="scroll":style="{ position: fixed ? 'fixed' : 'relative', zIndex: 1993 }"><viewclass="v-tabs__container":style="{display: scroll ? 'inline-flex' : 'flex',whiteSpace: scroll ? 'nowrap' : 'normal',background: bgColor,height,padding}"><viewclass="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><viewv-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><viewv-else:class="['v-tabs__container-pills', {animation: lineAnimation}]":style="{background: pillsColor,borderRadius: pillsBorderRadius,left: pillsLeft + 'px',width: currentWidth + 'px',height}"></view></view></scroll-view><viewclass="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: {type: Number,default: 0},tabs: {type: Array,default () {return []}},bgColor: {type: String,default: '#fff'},padding: {type: String,default: '0'},color: {type: String,default: '#333'},activeColor: {type: String,default: '#2979ff'},fontSize: {type: String,default: '28rpx'},activeFontSize: {type: String,default: '32rpx'},bold: {type: Boolean,default: true},scroll: {type: Boolean,default: true},height: {type: String,default: '70rpx'},lineColor: {type: String,default: '#2979ff'},lineHeight: {type: String,default: '10rpx'},lineScale: {type: Number,default: 0.5},lineRadius: {type: String,default: '10rpx'},pills: {type: Boolean,default: false},pillsColor: {type: String,default: '#2979ff'},pillsBorderRadius: {type: String,default: '10rpx'},field: {type: String,default: ''},fixed: {type: Boolean,default: false},paddingItem: {type: String,default: '0 22rpx'},lineAnimation: {type: Boolean,default: true}},data () {return {elId: '',lineWidth: 30,currentWidth: 0, // 当前选项的宽度lineLeft: 0, // 滑块距离左侧的位置pillsLeft: 0, // 胶囊距离左侧的位置scrollLeft: 0, // 距离左边的位置containerWidth: 0, // 容器的宽度current: 0 // 当前选中项}},watch: {value (newVal) {this.current = newValthis.$nextTick(() => {this.getTabItemWidth()})},current (newVal) {this.$emit('input', newVal)},tabs (newVal) {this.$nextTick(() => {this.getTabItemWidth()})}},methods: {// 产生随机字符串randomString (len) {len = len || 32let $chars ='ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/let maxPos = $chars.lengthlet 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 = indexthis.$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 = 0let currentWidth = 0if (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.valuethis.$nextTick(() => {this.getTabItemWidth()})}}</script><stylelang="scss"scoped>.v-tabs {width: 100%;box-sizing: border-box;overflow: hidden;::-webkit-scrollbar {display: none;}&__container {min-width: 100%;position: relative;display: inline-flex;align-items: center;white-space: nowrap;overflow: hidden;&-item {display: flex;align-items: center;height: 100%;position: relative;z-index: 10;// padding: 0 11px;transition: all 0.3s;white-space: nowrap;}&-line {position: absolute;bottom: 0;}&-pills {position: absolute;z-index: 9;}&-line,&-pills {&.animation {transition: all 0.3s linear;}}}}</style>
觉得有用?点个「在看」收藏起来,下次做项目直接抄作业!
夜雨聆风