2025-09-05

漂亮的Tabs标签


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: {
/**
 * 查找事件目标的指定标签名的父节点
 * @param {Event} e 事件对象
 * @param {String} tagName 要查找的标签名(大写)
 * @returns {HTMLElement|null} 找到的父节点或null
 */
useParentNode(e, tagName) {
  if (!e || !e.target || !tagName) return null;

  let el = e.target;
  // 向上遍历DOM树查找指定标签名的父节点
  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() {
      // 销毁时停止观察,避免内存泄漏
    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>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。