实现的效果

企业微信截图_17682843542751.png

企业微信截图_17682843744380.png
1 常用的参数配置 props.js
export default {
value: {
type: Number,
default: 0
},
modelValue: {
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: false
},
scroll: {
type: Boolean,
default: true
},
height: {
type: String,
default: '70rpx'
},
lineColor: {
type: String,
default: '#2979ff'
},
lineHeight: {
type: [String, Number],
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
},
zIndex: {
type: Number,
default: 1993
}
}
2. utils.js 防抖节流的方法
/**
* 函数节流器。
* 通过限制函数调用的频率,防止在高频率事件(如窗口滚动或鼠标移动)中过多调用给定的函数,从而优化性能。
*
* @param {Function} fn 要节流的函数。
* @param {number} delay 延迟的毫秒数,在这段时间内只能调用一次给定的函数。
* @returns {Function} 返回一个新的节流函数,它将控制原始函数的调用频率。
*/
export function throttle(fn, delay) {
// 用于存储定时器ID
let timeoutId
// 用于记录上一次函数执行的时间
let lastExecuted = 0
// 返回一个节流函数
return function () {
// 保存当前上下文和参数
const context = this
const args = arguments
// 获取当前时间
const now = Date.now()
// 计算剩余时间
const remaining = delay - (now - lastExecuted)
// 实际执行函数的内部函数
function execute() {
lastExecuted = now
// 在当前上下文中调用原始函数,并传入参数
fn.apply(context, args)
}
// 如果剩余时间小于等于0,表示可以执行函数
if (remaining <= 0) {
// 如果存在定时器,则清除定时器
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
// 执行函数
execute()
} else {
// 如果不存在定时器,则设置定时器
if (!timeoutId) {
timeoutId = setTimeout(() => {
timeoutId = null
// 执行函数
execute()
}, remaining)
}
}
}
}
/**
* 函数防抖动封装。
* 函数防抖(debounce)是指在事件被触发n秒后,才执行回调,如果在这n秒内事件又被触发,则重新计时。
* 主要用于限制函数调用的频率,常用于输入事件处理函数(如输入框的keyup事件)和窗口大小调整事件等。
*
* @param {Function} fn 需要被延迟执行的函数。
* @param {number} delay 延迟执行的时间,单位为毫秒。
* @returns {Function} 返回一个经过防抖动处理的函数。
*/
export function debounce(fn, delay) {
// 用于存储定时器的变量
let timer = null
// 返回一个封装函数
return function () {
// 如果定时器存在,则清除之前的定时器
if (timer) clearTimeout(timer)
// 设置新的定时器,延迟执行原函数
timer = setTimeout(() => {
// 使用apply确保函数在正确的上下文中执行,并传递所有参数
fn.apply(this, arguments)
}, delay)
}
}
3. v-tabs.vue 具体的页面及逻辑 兼容横竖屏幕 的
<template>
<view class="v-tabs">
<scroll-view
:id="getDomId"
:scroll-x="scroll"
:scroll-left="scroll ? scrollLeft : 0"
:scroll-with-animation="scroll"
:style="{ position: fixed ? 'fixed' : 'relative', zIndex, width: '100%' }"
>
<view
class="v-tabs__container"
:style="{
display: scroll ? 'inline-flex' : 'flex',
whiteSpace: scroll ? 'nowrap' : 'normal',
background: bgColor,
height,
padding
}"
>
<!-- Tab项:强制平分宽度 + 居中 -->
<view
:class="['v-tabs__container-item', { disabled: !!v.disabled }, { active: current == i }]"
v-for="(v, i) in tabs"
:key="i"
:style="{
color: current == i ? activeColor : color,
fontSize: fontSize,
fontWeight: bold && current == i ? 'bold' : 'normal',
justifyContent: 'center',
flex: 1,
padding: paddingItem
}"
@click="handleTabClick(i)"
>
<slot :row="v" :index="i">{{ field ? v[field] : v }}</slot>
</view>
<!-- 下划线:横屏专用定位 -->
<template v-if="!!tabs.length && !pills">
<view
class="v-tabs__container-line"
:class="{ animation: lineAnimation }"
:style="{
background: lineColor,
width: lineWidth + 'px',
height: lineHeight,
borderRadius: lineRadius,
left: lineLeft + 'px',
bottom: 0
}"
/>
</template>
<!-- 胶囊样式:横屏专用定位 -->
<template v-if="!!tabs.length && pills">
<view
class="v-tabs__container-pills"
:class="{ animation: lineAnimation }"
:style="{
background: pillsColor,
borderRadius: pillsBorderRadius,
width: pillsWidth + 'px',
height: pillsHeight,
left: pillsLeft + 'px',
top: '50%',
transform: 'translateY(-50%)'
}"
/>
</template>
</view>
</scroll-view>
<!-- fixed 占位符 -->
<view class="v-tabs__placeholder" :style="{ height: fixed ? height : '0', padding }"></view>
</view>
</template>
<script>
import { throttle } from './utils'
import props from './props'
/**
* v-tabs 横屏专用零bug版
* 核心特性:
* 1. 横屏初始化100%对齐
* 2. 无需依赖DOM位置读取
* 3. 自动适配横屏宽度变化
* 4. 下划线/胶囊永不偏移
*/
export default {
name: 'VTabs',
props,
// #ifdef VUE3
emits: ['update:modelValue', 'change'],
// #endif
data() {
return {
current: 0, // 当前选中下标
scrollLeft: 0, // 滚动位置
lineLeft: 0, // 下划线左侧偏移
lineWidth: 0, // 下划线宽度
pillsLeft: 0, // 胶囊左侧偏移
pillsWidth: 0, // 胶囊宽度
pillsHeight: '80%',// 胶囊高度
containerWidth: 0 // 容器宽度
}
},
computed: {
// 生成唯一ID(简化版,避免随机数导致的问题)
getDomId() {
return `v-tabs-${this._uid}`
},
// 单个Tab宽度(横屏核心)
singleTabWidth() {
return this.containerWidth / this.tabs.length
}
},
watch: {
// 监听选中值变化(兼容Vue2/Vue3)
// #ifdef VUE3
modelValue: {
immediate: true,
handler(newVal) {
this.current = newVal > -1 && newVal < this.tabs.length ? newVal : 0
this.updatePosition()
}
},
// #endif
// #ifdef VUE2
value: {
immediate: true,
handler(newVal) {
this.current = newVal > -1 && newVal < this.tabs.length ? newVal : 0
this.updatePosition()
}
},
// #endif
// 监听tabs变化,重新计算位置
tabs: {
immediate: true,
handler() {
this.initContainerWidth()
}
}
},
mounted() {
// 初始化容器宽度(横屏专用)
this.initContainerWidth()
// 监听屏幕旋转(横屏核心)
// #ifdef APP-PLUS
plus.screen.addEventListener('orientationchange', () => {
setTimeout(() => {
this.initContainerWidth()
}, 200)
})
// #endif
},
methods: {
// 初始化容器宽度(横屏稳定后读取)
initContainerWidth() {
// 延迟执行,确保横屏DOM稳定
setTimeout(() => {
const query = uni.createSelectorQuery().in(this)
query.select(`#${this.getDomId}`).boundingClientRect(rect => {
if (rect && rect.width) {
this.containerWidth = rect.width
this.updatePosition()
}
}).exec()
}, 300)
},
// Tab点击事件(防抖+禁用判断)
handleTabClick: throttle(function(index) {
const isDisabled = !!this.tabs[index]?.disabled
if (this.current !== index && !isDisabled) {
this.current = index
// 触发事件(兼容Vue2/Vue3)
// #ifdef VUE3
this.$emit('update:modelValue', index)
// #endif
// #ifdef VUE2
this.$emit('input', index)
// #endif
this.$emit('change', index)
this.updatePosition()
}
}, 300),
// 更新下划线/胶囊位置(横屏核心算法)
updatePosition() {
if (!this.containerWidth || !this.tabs.length) return
// 计算单个Tab宽度
const tabWidth = this.singleTabWidth
// 计算当前Tab起始位置
const tabStart = this.current * tabWidth
// 下划线配置(横屏精准对齐)
this.lineWidth = tabWidth * (this.lineScale || 0.8) // 下划线宽度
this.lineLeft = tabStart + (tabWidth - this.lineWidth) / 2 // 下划线居中
// 胶囊配置
this.pillsWidth = tabWidth * 0.9
this.pillsLeft = tabStart + (tabWidth - this.pillsWidth) / 2
},
// 暴露给父组件的手动更新方法
update() {
this.initContainerWidth()
}
}
}
</script>
<style lang="scss" scoped>
.v-tabs {
width: 100%;
box-sizing: border-box;
overflow: hidden;
/* 隐藏滚动条 */
/* #ifdef H5 */
::-webkit-scrollbar {
display: none;
}
/* #endif */
&__container {
min-width: 100%;
position: relative;
display: flex;
align-items: center;
overflow: hidden;
&-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
z-index: 10;
transition: color 0.3s ease;
white-space: nowrap;
&.disabled {
opacity: 0.5;
color: #999;
pointer-events: none;
}
}
&-line {
position: absolute;
transition: left 0.3s ease;
transform: translateZ(0); // 消除像素偏差
}
&-pills {
position: absolute;
transition: left 0.3s ease;
transform: translateZ(0); // 消除像素偏差
z-index: 9;
}
&-line.animation,
&-pills.animation {
transition: all 0.3s linear;
}
}
&__placeholder {
box-sizing: border-box;
}
}
</style>
4. 使用说明
<template>
<view>
<!-- 横屏平分模式(推荐) -->
<v-tabs
ref="tabsRef"
v-model="currentTab"
:tabs="tabsList"
:scroll="false" <!-- 横屏强制平分宽度 -->
:line-scale="0.8" <!-- 下划线宽度占Tab的80% -->
lineColor="#007aff"
activeColor="#007aff"
color="#666"
height="70rpx"
fontSize="28rpx"
@change="handleTabChange"
></v-tabs>
<!-- 竖屏滚动模式(Tab数量多时) -->
<v-tabs
v-model="currentTab"
:tabs="longTabsList"
:scroll="true" <!-- 开启滚动 -->
paddingItem="0 22rpx" <!-- 单个Tab左右内边距 -->
:pills="true" <!-- 胶囊样式 -->
pillsColor="#f56c6c"
></v-tabs>
</view>
</template>
<script setup>
import { ref, onShow } from 'vue'
import VTabs from '@/components/v-tabs/v-tabs.vue'
const tabsRef = ref(null)
const currentTab = ref(0)
// 基础Tab列表(字符串数组)
const tabsList = ['超载数量', '牲畜数量', '流转面积', '奖补面积']
// 长Tab列表(对象数组,配合field使用)
const longTabsList = [
{ id: 1, name: '首页' },
{ id: 2, name: '数据统计' },
{ id: 3, name: '报表分析' },
{ id: 4, name: '系统设置' },
{ id: 5, name: '帮助中心' }
]
// Tab切换事件
const handleTabChange = (index) => {
console.log('选中第', index + 1, '个Tab')
}
// 可选:横屏后手动触发更新(双重保障)
onShow(() => {
setTimeout(() => {
tabsRef.value?.update()
}, 500)
})
</script>
5 参数说明
value Number 0 Vue2 双向绑定值(选中 Tab 的下标)
modelValue Number 0 Vue3 双向绑定值(选中 Tab 的下标)
tabs Array [] Tab 列表数据:
1. 字符串数组:['标题1', '标题2']
2. 对象数组:需配合field使用
bgColor String '#fff' Tab 栏背景色
padding String '0' Tab 容器整体内边距(如 10rpx 0)
color String '#333' 未选中 Tab 的文字颜色
activeColor String '#2979ff' 选中 Tab 的文字颜色
fontSize String '28rpx' 默认文字大小
activeFontSize String '32rpx' 选中 Tab 的文字大小(优先级高于 fontSize)
bold Boolean false 选中 Tab 的文字是否加粗
scroll Boolean true 是否开启横向滚动:
true - 滚动模式(Tab 数量多)
false - 平分模式(横屏推荐)
height String '70rpx' Tab 栏整体高度(如 80rpx)
lineColor String '#2979ff' 下划线颜色(非胶囊模式生效)
lineHeight String/Number '10rpx' 下划线高度(如 8rpx)
lineScale Number 0.5 下划线宽度比例(相对于单个 Tab 宽度,0-1 之间,如 0.8 = 占 80%)
lineRadius String '10rpx' 下划线圆角(如 5rpx)
pills Boolean false 是否开启胶囊样式:
true - 胶囊背景(覆盖下划线)
false - 下划线样式
pillsColor String '#2979ff' 胶囊背景色(胶囊模式生效)
pillsBorderRadius String '10rpx' 胶囊圆角(如 20rpx,推荐设为高度的一半实现圆形)
field String '' 对象数组时指定显示字段(如 tabs=[{name:'标题'}],field='name')
fixed Boolean false 是否固定在顶部:
true - 固定(自动生成占位符)
false - 相对定位
paddingItem String '0 22rpx' 单个 Tab 的内边距(滚动模式下控制左右间距)
lineAnimation Boolean true 是否开启下划线 / 胶囊切换动画
zIndex Number 1993 Tab 栏层级(fixed=true 时建议提高)
事件说明
change Tab 被点击且切换成功时 index: Number 返回选中 Tab 的下标(从 0 开始)
input Vue2 双向绑定触发 index: Number 同 change,适配 Vue2 v-model
update:modelValue Vue3 双向绑定触发 index: Number 同 change,适配 Vue3 v-model