漂亮的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>