perf: optimization of tabbar display (#4169)

* perf: optimization of tabbar display

* fix: ci error

* chore: typo

* chore: typo
This commit is contained in:
Vben
2024-08-16 22:20:18 +08:00
committed by GitHub
parent 8987067b5a
commit 0faf7810b6
38 changed files with 710 additions and 504 deletions

View File

@@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types';
import { computed, ref, watch } from 'vue';
import { computed, ref } from 'vue';
import { MdiPin, X } from '@vben-core/icons';
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
interface Props extends TabsProps {}
@@ -20,17 +20,17 @@ const props = withDefaults(defineProps<Props>(), {
contentClass: 'vben-tabs-content',
contextMenus: () => [],
gap: 7,
maxWidth: 150,
minWidth: 80,
tabs: () => [],
});
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
const emit = defineEmits<{
close: [string];
unpin: [TabDefinition];
}>();
const active = defineModel<string>('active');
const contentRef = ref();
const tabRef = ref();
const tabWidth = ref<number>(props.maxWidth);
const style = computed(() => {
const { gap } = props;
@@ -53,148 +53,118 @@ const tabsView = computed((): TabConfig[] => {
};
});
});
watch(active, () => {
scrollIntoView();
});
function scrollIntoView() {
setTimeout(() => {
const element = document.querySelector(`.tabs-chrome__item.is-active`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
</script>
<template>
<div :style="style" class="tabs-chrome size-full flex-1 overflow-hidden pt-1">
<VbenScrollbar
id="tabs-scrollbar"
class="tabs-chrome__scrollbar h-full"
horizontal
scroll-bar-class="z-10 hidden"
>
<!-- footer -> 4px -->
<div
ref="contentRef"
:class="contentClass"
:style="style"
class="tabs-chrome !flex h-full w-max pr-6"
>
<TransitionGroup name="slide-left">
<div
ref="contentRef"
:class="contentClass"
class="relative !flex h-full w-max"
v-for="(tab, i) in tabsView"
:key="tab.key"
ref="tabRef"
:class="[{ 'is-active': tab.key === active, dragable: !tab.affixTab }]"
:data-active-tab="active"
:data-index="i"
class="tabs-chrome__item draggable group relative -mr-3 flex h-full select-none items-center"
data-tab-item="true"
@click="active = tab.key"
>
<TransitionGroup name="slide-left">
<div
v-for="(tab, i) in tabsView"
:key="tab.key"
ref="tabRef"
:class="[
{ 'is-active': tab.key === active, dragable: !tab.affixTab },
]"
:data-active-tab="active"
:data-index="i"
:style="{
width: `${tabWidth}px`,
left: `${(tabWidth - gap * 2) * i}px`,
}"
class="tabs-chrome__item group absolute flex h-full select-none items-center transition-all"
@click="active = tab.key"
>
<VbenContextMenu
:handler-data="tab"
:menus="contextMenus"
:modal="false"
item-class="pr-6"
<VbenContextMenu
:handler-data="tab"
:menus="contextMenus"
:modal="false"
item-class="pr-6"
>
<div class="relative size-full px-1">
<!-- divider -->
<div
v-if="i !== 0 && tab.key !== active"
class="tabs-chrome__divider bg-foreground/50 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
></div>
<!-- background -->
<div
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
>
<div class="size-full">
<!-- divider -->
<div
v-if="i !== 0 && tab.key !== active"
class="tabs-chrome__divider bg-foreground/60 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
></div>
<!-- background -->
<div
class="tabs-chrome__background absolute z-[1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
>
<div
class="tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
></div>
<svg
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
</svg>
<svg
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
</svg>
</div>
<div
class="tabs-chrome__background-content group-[.is-active]:bg-heavy dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
></div>
<svg
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
</svg>
<svg
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
</svg>
</div>
<!-- extra -->
<div
class="tabs-chrome__extra absolute right-[calc(var(--gap)*1.5)] top-1/2 z-[3] size-4 translate-y-[-50%]"
>
<!-- close-icon -->
<X
v-show="
!tab.affixTab && tabsView.length > 1 && tab.closable
"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary mt-[2px] size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
<MdiPin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- extra -->
<div
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
>
<!-- close-icon -->
<X
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
<MdiPin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- tab-item-main -->
<div
class="tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground absolute left-0 right-0 z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-4 duration-150 group-hover:pr-3"
>
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden"
fallback
/>
<!-- tab-item-main -->
<div
class="tabs-chrome__item-main group-[.is-active]:text-accent-foreground dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150"
>
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-1 flex size-4 items-center overflow-hidden"
fallback
/>
<span
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap text-sm"
>
{{ tab.title }}
</span>
</div>
</div>
</VbenContextMenu>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
{{ tab.title }}
</span>
</div>
</div>
</TransitionGroup>
</VbenContextMenu>
</div>
<!-- footer -->
<!-- <div class="bg-background h-1"></div> -->
</VbenScrollbar>
</TransitionGroup>
</div>
</template>
<style scoped>
.tabs-chrome {
.dragging {
.tabs-chrome__item-main {
/* .dragging { */
/* .tabs-chrome__item-main {
@apply pr-0;
}
} */
.tabs-chrome__extra {
/* .tabs-chrome__extra {
@apply hidden;
}
}
} */
/* } */
&__item:not(.dragging) {
@apply cursor-pointer;
&__item {
&:hover:not(.is-active) {
& + .tabs-chrome__item {
.tabs-chrome__divider {
@@ -207,13 +177,10 @@ function scrollIntoView() {
}
.tabs-chrome__background {
&-content {
@apply bg-accent mx-1 rounded-md pb-2;
}
@apply pb-[2px];
&-before,
&-after {
@apply fill-primary/0;
&-content {
@apply bg-accent-hover mx-[2px] rounded-md;
}
}
}
@@ -226,30 +193,7 @@ function scrollIntoView() {
@apply opacity-0 !important;
}
}
.tabs-chrome__background {
@apply opacity-100;
/* &-content {
@apply bg-accent;
}
&-before,
&-after {
@apply fill-heavy;
} */
}
}
}
&__scrollbar,
&__label {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
}
</style>

View File

@@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types';
import { computed, watch } from 'vue';
import { computed } from 'vue';
import { MdiPin, X } from '@vben-core/icons';
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
interface Props extends TabsProps {}
@@ -21,7 +21,10 @@ const props = withDefaults(defineProps<Props>(), {
tabs: () => [],
});
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
const emit = defineEmits<{
close: [string];
unpin: [TabDefinition];
}>();
const active = defineModel<string>('active');
const typeWithClass = computed(() => {
@@ -55,108 +58,71 @@ const tabsView = computed((): TabConfig[] => {
};
});
});
watch(active, () => {
scrollIntoView();
});
function scrollIntoView() {
setTimeout(() => {
const element = document.querySelector(`.tabs-chrome__item.is-active`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
</script>
<template>
<div class="size-full flex-1 overflow-hidden">
<VbenScrollbar
id="tabs-scrollbar"
class="tabs-scrollbar h-full"
horizontal
scroll-bar-class="z-10 hidden"
>
<div
:class="contentClass"
class="relative !flex h-full w-max items-center pr-6"
>
<TransitionGroup name="slide-left">
<div
:class="contentClass"
class="relative !flex h-full w-max items-center"
v-for="(tab, i) in tabsView"
:key="tab.key"
:class="[
{
'is-active dark:bg-accent bg-primary/15': tab.key === active,
dragable: !tab.affixTab,
},
typeWithClass.content,
]"
:data-index="i"
class="tab-item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none"
data-tab-item="true"
@click="active = tab.key"
>
<TransitionGroup name="slide-left">
<div
v-for="(tab, i) in tabsView"
:key="tab.key"
:class="[
{
'is-active dark:bg-accent bg-primary/15': tab.key === active,
dragable: !tab.affixTab,
},
typeWithClass.content,
]"
:data-index="i"
class="tabs-chrome__item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none transition-all duration-300"
@click="active = tab.key"
>
<VbenContextMenu
:handler-data="tab"
:menus="contextMenus"
:modal="false"
item-class="pr-6"
<VbenContextMenu
:handler-data="tab"
:menus="contextMenus"
:modal="false"
item-class="pr-6"
>
<div class="relative flex size-full items-center">
<!-- extra -->
<div
class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden"
>
<div class="relative flex size-full items-center">
<!-- extra -->
<div
class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden"
>
<!-- close-icon -->
<X
v-show="
!tab.affixTab && tabsView.length > 1 && tab.closable
"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
<MdiPin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- close-icon -->
<X
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
<MdiPin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- tab-item-main -->
<div
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
>
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden"
fallback
/>
<!-- tab-item-main -->
<div
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
>
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden"
fallback
/>
<span
class="flex-1 overflow-hidden whitespace-nowrap text-sm"
>
{{ tab.title }}
</span>
</div>
</div>
</VbenContextMenu>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
{{ tab.title }}
</span>
</div>
</div>
</TransitionGroup>
</VbenContextMenu>
</div>
</VbenScrollbar>
</TransitionGroup>
</div>
</template>
<style scoped>
.tabs-scrollbar {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
</style>