167 lines
4.1 KiB
Vue

<template>
<div class="k-tabs">
<div class="k-tabs-tags">
<div class="k-tabs-tag-item" v-for="pane of tabsContainer.paneInfos" :key="pane.id"
:ref="el => tabsContainer.getPanes(el, pane.id)" @click="tabsContainer.switchLabel(pane.id)" :class="{
'active-tab': tabsContainer.lastPaneId === pane.id,
'prev-tab': pane.id === prevActiveIndex
}">
<span :class="pane.labelClass"></span>
<span>{{ pane.label }}</span>
</div>
</div>
<div class="k-tabs-content" :ref="el => tabsContainer.panelContainer = el">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, useSlots, nextTick, onMounted, ref } from 'vue';
import gsap from 'gsap';
type PaneInfo = {
id: number;
label: string;
labelClass: string;
};
const slots = useSlots();
const prevActiveIndex = ref<number | null>(null);
const tabsContainer = reactive({
paneInfos: [] as PaneInfo[],
panes: [] as HTMLElement[],
lastPaneId: 0,
panelContainer: undefined as HTMLElement | undefined,
getPanes(el: HTMLElement | null, id: number) {
if (el) this.panes[id] = el;
},
async switchLabel(id: number) {
if (this.lastPaneId === id) return;
const oldId = this.lastPaneId;
prevActiveIndex.value = oldId;
this.lastPaneId = id;
const container = this.panelContainer;
if (!container) return;
const panels = Array.from(container.children) as HTMLElement[];
const oldPanel = panels[oldId];
const newPanel = panels[id];
// 1. First shrink the old panel to 0.3 scale
if (oldPanel) {
await gsap.to(oldPanel, {
scale: 0.3,
opacity: 0,
duration: 0.3,
ease: "power2.in"
});
}
// 2. Hide all other panels
panels.forEach((panel, index) => {
if (index !== id) {
gsap.set(panel, {
display: 'none',
opacity: 0,
scale: 1
});
}
});
// 3. Show and animate in the new panel
gsap.set(newPanel, {
display: 'block',
opacity: 0,
scale: 1.2
});
gsap.to(newPanel, {
opacity: 1,
scale: 1,
duration: 0.4,
ease: "back.out(1.2)"
});
},
updateLabels() {
this.paneInfos = (slots.default?.() || []).map((vnode, index) => ({
id: index,
label: vnode.props?.label || '',
labelClass: vnode.props?.labelClass || '',
}));
}
});
// Initialize
tabsContainer.updateLabels();
onMounted(() => {
if (tabsContainer.panelContainer) {
const panels = Array.from(tabsContainer.panelContainer.children) as HTMLElement[];
panels.forEach((panel, index) => {
panel.style.position = 'absolute';
panel.style.width = '100%';
if (index !== 0) {
panel.style.display = 'none';
} else {
gsap.from(panel, {
scale: 1.2,
opacity: 0,
duration: 0.5
});
}
});
}
});
</script>
<style scoped>
.k-tabs {
position: relative;
}
.k-tabs-tags {
display: flex;
margin-bottom: 20px;
position: relative;
z-index: 10;
gap: 8px;
}
.k-tabs-tag-item {
background-color: var(--vp-button-alt-bg);
border-radius: .5em;
padding: 6px 16px;
cursor: pointer;
transition: all 0.3s ease;
transform-origin: center;
will-change: transform;
}
.k-tabs-tag-item.active-tab {
background-color: var(--vp-c-brand-3);
transform: scale(1.05);
}
.k-tabs-content {
position: relative;
min-height: 200px;
}
.k-tabs-content>* {
position: absolute;
top: 0;
left: 0;
width: 100%;
will-change: transform, opacity;
backface-visibility: hidden;
transform-origin: center center;
}
</style>