tab 選擇欄的封裝

實(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容