2025-09-05

漂亮的Tabs標(biāo)簽


image.png

使用方法

  <mTabs
      v-model="activeTab"
      :options="tabOptions"
      @change="handleTabChange"
   />

組件頁面

<template>
  <div
    :id="id"
    class="tabs-list-container"
    :class="{ 'flex-col': direction === 'vertical' }"
    >
<div class="tab-list-item-selected" />
<a
  v-for="item in options"
  :key="item.value"
  class="tab-list-item text-left"
  :class="{
    active: item.value === value,
    'w-full': direction === 'horizontal',
    'h-full': direction === 'vertical',
  }"
  :style="{
    'justify-content': align,
  }"
  @click="(e) => handleClick(e, item)"
>
  <slot name="default" :item="item">
    <ma-svg-icon v-if="item.icon" :name="item.icon" :size="16" />
    <span>{{
      typeof item.label === "function" ? item.label() : item.label
    }}</span>
  </slot>
</a>
</div>
</template>

 <script>
  export default {
    name: "MTabs",
    props: {
      options: {
        type: Array,
        required: true,
        default: () => [],
      },
      direction: {
        type: String,
        default: "horizontal",
        validator: (value) => ["horizontal", "vertical"].includes(value),
      },
      align: {
        type: String,
        default: "center",
        validator: (value) => ["start", "center", "end"].includes(value),
      },
      value: {
        type: [String, Number],
        required: true,
      },
    },
  data() {
    return {
      id: `tabDomId_${Math.floor(
         Math.random() * 100000 + Math.random() * 20000 + Math.random() * 5000
      )}`,
      selectedEl: null,
      resizeObserver: null,
    };
  },
methods: {
/**
 * 查找事件目標(biāo)的指定標(biāo)簽名的父節(jié)點(diǎn)
 * @param {Event} e 事件對象
 * @param {String} tagName 要查找的標(biāo)簽名(大寫)
 * @returns {HTMLElement|null} 找到的父節(jié)點(diǎn)或null
 */
useParentNode(e, tagName) {
  if (!e || !e.target || !tagName) return null;

  let el = e.target;
  // 向上遍歷DOM樹查找指定標(biāo)簽名的父節(jié)點(diǎn)
  while (el && el.nodeName !== tagName.toUpperCase()) {
    el = el.parentNode;
    // 防止遍歷到document還沒找到的情況
    if (el === document) {
      el = null;
      break;
    }
  }
  return el;
},

handleClick(e, item) {
  e.preventDefault();
  if (this.value !== item.value) {
    this.$emit("input", item.value);
    const node = this.useParentNode(e, "a");
    this.setSelectedElStyle(node);
    this.$emit("change", item.value, item);
  }
},

setSelectedElStyle(node) {
  if (this.selectedEl) {
    if (this.direction === "vertical") {
      this.selectedEl.style.height = `${node.offsetHeight}px`;
      this.selectedEl.style.width = `${node.offsetWidth}px`;
      this.selectedEl.style.transform = `translateY(${
        node.offsetTop - 4
      }px)`;
    } else {
      this.selectedEl.style.height = `${node.offsetHeight}px`;
      this.selectedEl.style.width = `${node.offsetWidth}px`;
      this.selectedEl.style.transform = `translateX(${
        node.offsetLeft - 4
      }px)`;
    }
  }
},

   initSelectedElStyle() {
      const node = document.querySelector(`#${this.id} .tab-list-item.active`);
      if (node) {
        this.setSelectedElStyle(node);
      }
   },
},
 watch: {
  options: {
      handler() {
          this.$nextTick(() => {
            this.initSelectedElStyle();
          });
      },
      deep: true,
   },
   direction() {
      this.$nextTick(() => {
          this.initSelectedElStyle();
      });
    },
    value() {
      this.$nextTick(() => {
        this.initSelectedElStyle();
      });
    },
  },
  mounted() {
      this.selectedEl = document.querySelector(
        `#${this.id} .tab-list-item-selected`
      );
    // 用原生 ResizeObserver 替代 useResizeObserver
    this.resizeObserver = new ResizeObserver(() => {
      this.initSelectedElStyle();
    });
    this.resizeObserver.observe(document.body);
  },
  beforeDestroy() {
      // 銷毀時(shí)停止觀察,避免內(nèi)存泄漏
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
    }
  },
};
</script>

<style lang="scss" scoped>
.tabs-list-container {
  position: relative;
  display: flex;
  border-radius: 4px;
  background-color: rgb(243, 244, 246, 1);
  padding: 4px;
  flex-grow: 1;
  justify-items: flex-start;

  font-size: 14px;
  line-height: 20px;
  height: 36px;
  width: 100%;
}

.tab-list-item {
  position: relative;
  z-index: 3;
  display: flex;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  border-radius: 0.25rem;
  padding: 0.375rem 0.5rem;
  color: rgb(107, 114, 128);
  transition-property: all;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 0.15s;
  transition-duration: 0.5s;
  width: 100%;
}

.tab-list-item.active {
  color: rgb(68, 64, 60);
}

.tab-list-item-selected {
  position: absolute;
  z-index: 2;
  border-radius: 0.25rem;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
  height: calc(100% - 8px);
  transition: transform 0.3s;
}
</style>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 作品簡介 這是個(gè)真實(shí)存在的社會(huì)現(xiàn)實(shí)生活中的故事,但是以文學(xué)體裁小說的形式寫出來的。 社會(huì)很復(fù)雜,各...
    張東北閱讀 116評(píng)論 0 0
  • 組態(tài)配方時(shí),變量為灰色無法選擇,變量在畫面中已被使用過,則無法再通過配方來使用方法時(shí)新建需要配方組態(tài)的變量,1新建...
    haha_0702閱讀 43評(píng)論 0 0
  • 即時(shí)設(shè)計(jì)的優(yōu)點(diǎn) 云端協(xié)作與跨平臺(tái)支持 無需安裝客戶端,瀏覽器即可使用,支持多人在線實(shí)時(shí)協(xié)作,適配Windows/m...
    丹妮滸閱讀 138評(píng)論 0 0
  • 一覺醒來5:20。沒有再睡,打開手機(jī)。多鄰國過關(guān)。 這還玩兒的有了癮兒了,和小燕兒兩個(gè)人抱著比賽著打積分兒。 19...
    飄蕩著的那朵閱讀 48評(píng)論 0 1
  • 高速陶瓷雕銑機(jī):高轉(zhuǎn)速主軸,陶瓷雕刻快速成型不費(fèi)時(shí) 在陶瓷加工領(lǐng)域,“快速成型” 與 “加工精度” 的平衡始終是核...
    工業(yè)陶瓷閱讀 56評(píng)論 0 0

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