實(shí)現(xiàn)的效果

企業(yè)微信截圖_17682843542751.png

企業(yè)微信截圖_17682843744380.png
1 常用的參數(shù)配置 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 防抖節(jié)流的方法
/**
* 函數(shù)節(jié)流器。
* 通過限制函數(shù)調(diào)用的頻率,防止在高頻率事件(如窗口滾動或鼠標(biāo)移動)中過多調(diào)用給定的函數(shù),從而優(yōu)化性能。
*
* @param {Function} fn 要節(jié)流的函數(shù)。
* @param {number} delay 延遲的毫秒數(shù),在這段時間內(nèi)只能調(diào)用一次給定的函數(shù)。
* @returns {Function} 返回一個新的節(jié)流函數(shù),它將控制原始函數(shù)的調(diào)用頻率。
*/
export function throttle(fn, delay) {
// 用于存儲定時器ID
let timeoutId
// 用于記錄上一次函數(shù)執(zhí)行的時間
let lastExecuted = 0
// 返回一個節(jié)流函數(shù)
return function () {
// 保存當(dāng)前上下文和參數(shù)
const context = this
const args = arguments
// 獲取當(dāng)前時間
const now = Date.now()
// 計(jì)算剩余時間
const remaining = delay - (now - lastExecuted)
// 實(shí)際執(zhí)行函數(shù)的內(nèi)部函數(shù)
function execute() {
lastExecuted = now
// 在當(dāng)前上下文中調(diào)用原始函數(shù),并傳入?yún)?shù)
fn.apply(context, args)
}
// 如果剩余時間小于等于0,表示可以執(zhí)行函數(shù)
if (remaining <= 0) {
// 如果存在定時器,則清除定時器
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
// 執(zhí)行函數(shù)
execute()
} else {
// 如果不存在定時器,則設(shè)置定時器
if (!timeoutId) {
timeoutId = setTimeout(() => {
timeoutId = null
// 執(zhí)行函數(shù)
execute()
}, remaining)
}
}
}
}
/**
* 函數(shù)防抖動封裝。
* 函數(shù)防抖(debounce)是指在事件被觸發(fā)n秒后,才執(zhí)行回調(diào),如果在這n秒內(nèi)事件又被觸發(fā),則重新計(jì)時。
* 主要用于限制函數(shù)調(diào)用的頻率,常用于輸入事件處理函數(shù)(如輸入框的keyup事件)和窗口大小調(diào)整事件等。
*
* @param {Function} fn 需要被延遲執(zhí)行的函數(shù)。
* @param {number} delay 延遲執(zhí)行的時間,單位為毫秒。
* @returns {Function} 返回一個經(jīng)過防抖動處理的函數(shù)。
*/
export function debounce(fn, delay) {
// 用于存儲定時器的變量
let timer = null
// 返回一個封裝函數(shù)
return function () {
// 如果定時器存在,則清除之前的定時器
if (timer) clearTimeout(timer)
// 設(shè)置新的定時器,延遲執(zhí)行原函數(shù)
timer = setTimeout(() => {
// 使用apply確保函數(shù)在正確的上下文中執(zhí)行,并傳遞所有參數(shù)
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項(xiàng):強(qiáng)制平分寬度 + 居中 -->
<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, // 當(dāng)前選中下標(biāo)
scrollLeft: 0, // 滾動位置
lineLeft: 0, // 下劃線左側(cè)偏移
lineWidth: 0, // 下劃線寬度
pillsLeft: 0, // 膠囊左側(cè)偏移
pillsWidth: 0, // 膠囊寬度
pillsHeight: '80%',// 膠囊高度
containerWidth: 0 // 容器寬度
}
},
computed: {
// 生成唯一ID(簡化版,避免隨機(jī)數(shù)導(dǎo)致的問題)
getDomId() {
return `v-tabs-${this._uid}`
},
// 單個Tab寬度(橫屏核心)
singleTabWidth() {
return this.containerWidth / this.tabs.length
}
},
watch: {
// 監(jiān)聽選中值變化(兼容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
// 監(jiān)聽tabs變化,重新計(jì)算位置
tabs: {
immediate: true,
handler() {
this.initContainerWidth()
}
}
},
mounted() {
// 初始化容器寬度(橫屏專用)
this.initContainerWidth()
// 監(jiān)聽屏幕旋轉(zhuǎn)(橫屏核心)
// #ifdef APP-PLUS
plus.screen.addEventListener('orientationchange', () => {
setTimeout(() => {
this.initContainerWidth()
}, 200)
})
// #endif
},
methods: {
// 初始化容器寬度(橫屏穩(wěn)定后讀?。? initContainerWidth() {
// 延遲執(zhí)行,確保橫屏DOM穩(wěn)定
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點(diǎn)擊事件(防抖+禁用判斷)
handleTabClick: throttle(function(index) {
const isDisabled = !!this.tabs[index]?.disabled
if (this.current !== index && !isDisabled) {
this.current = index
// 觸發(fā)事件(兼容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
// 計(jì)算單個Tab寬度
const tabWidth = this.singleTabWidth
// 計(jì)算當(dāng)前Tab起始位置
const tabStart = this.current * tabWidth
// 下劃線配置(橫屏精準(zhǔn)對齊)
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" <!-- 橫屏強(qiáng)制平分寬度 -->
:line-scale="0.8" <!-- 下劃線寬度占Tab的80% -->
lineColor="#007aff"
activeColor="#007aff"
color="#666"
height="70rpx"
fontSize="28rpx"
@change="handleTabChange"
></v-tabs>
<!-- 豎屏滾動模式(Tab數(shù)量多時) -->
<v-tabs
v-model="currentTab"
:tabs="longTabsList"
:scroll="true" <!-- 開啟滾動 -->
paddingItem="0 22rpx" <!-- 單個Tab左右內(nèi)邊距 -->
: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)
// 基礎(chǔ)Tab列表(字符串?dāng)?shù)組)
const tabsList = ['超載數(shù)量', '牲畜數(shù)量', '流轉(zhuǎn)面積', '獎補(bǔ)面積']
// 長Tab列表(對象數(shù)組,配合field使用)
const longTabsList = [
{ id: 1, name: '首頁' },
{ id: 2, name: '數(shù)據(jù)統(tǒng)計(jì)' },
{ id: 3, name: '報表分析' },
{ id: 4, name: '系統(tǒng)設(shè)置' },
{ id: 5, name: '幫助中心' }
]
// Tab切換事件
const handleTabChange = (index) => {
console.log('選中第', index + 1, '個Tab')
}
// 可選:橫屏后手動觸發(fā)更新(雙重保障)
onShow(() => {
setTimeout(() => {
tabsRef.value?.update()
}, 500)
})
</script>
5 參數(shù)說明
value Number 0 Vue2 雙向綁定值(選中 Tab 的下標(biāo))
modelValue Number 0 Vue3 雙向綁定值(選中 Tab 的下標(biāo))
tabs Array [] Tab 列表數(shù)據(jù):
1. 字符串?dāng)?shù)組:['標(biāo)題1', '標(biāo)題2']
2. 對象數(shù)組:需配合field使用
bgColor String '#fff' Tab 欄背景色
padding String '0' Tab 容器整體內(nèi)邊距(如 10rpx 0)
color String '#333' 未選中 Tab 的文字顏色
activeColor String '#2979ff' 選中 Tab 的文字顏色
fontSize String '28rpx' 默認(rèn)文字大小
activeFontSize String '32rpx' 選中 Tab 的文字大?。▋?yōu)先級高于 fontSize)
bold Boolean false 選中 Tab 的文字是否加粗
scroll Boolean true 是否開啟橫向滾動:
true - 滾動模式(Tab 數(shù)量多)
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,推薦設(shè)為高度的一半實(shí)現(xiàn)圓形)
field String '' 對象數(shù)組時指定顯示字段(如 tabs=[{name:'標(biāo)題'}],field='name')
fixed Boolean false 是否固定在頂部:
true - 固定(自動生成占位符)
false - 相對定位
paddingItem String '0 22rpx' 單個 Tab 的內(nèi)邊距(滾動模式下控制左右間距)
lineAnimation Boolean true 是否開啟下劃線 / 膠囊切換動畫
zIndex Number 1993 Tab 欄層級(fixed=true 時建議提高)
事件說明
change Tab 被點(diǎn)擊且切換成功時 index: Number 返回選中 Tab 的下標(biāo)(從 0 開始)
input Vue2 雙向綁定觸發(fā) index: Number 同 change,適配 Vue2 v-model
update:modelValue Vue3 雙向綁定觸發(fā) index: Number 同 change,適配 Vue3 v-model