perf: optimization of tabbar display (#4169)

* perf: optimization of tabbar display

* fix: ci error

* chore: typo

* chore: typo
pull/48/MERGE
Vben 2024-08-16 22:20:18 +08:00 committed by GitHub
parent 8987067b5a
commit 0faf7810b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 710 additions and 504 deletions

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.3", "ant-design-vue": "^4.2.3",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"pinia": "2.2.2", "pinia": "2.2.2",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"element-plus": "^2.8.0", "element-plus": "^2.8.0",
"pinia": "2.2.2", "pinia": "2.2.2",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -40,10 +40,10 @@
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"naive-ui": "^2.39.0", "naive-ui": "^2.39.0",
"pinia": "2.2.2", "pinia": "2.2.2",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -14,6 +14,6 @@
"@nolebase/vitepress-plugin-git-changelog": "^2.4.0", "@nolebase/vitepress-plugin-git-changelog": "^2.4.0",
"@vite-pwa/vitepress": "^0.5.0", "@vite-pwa/vitepress": "^0.5.0",
"vitepress": "^1.3.2", "vitepress": "^1.3.2",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -42,7 +42,8 @@ export async function typescript(): Promise<Linter.Config[]> {
}, },
], ],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], // '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': [ '@typescript-eslint/no-empty-function': [

View File

@ -94,7 +94,7 @@
"node": ">=20", "node": ">=20",
"pnpm": ">=9" "pnpm": ">=9"
}, },
"packageManager": "pnpm@9.7.0", "packageManager": "pnpm@9.7.1",
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {

View File

@ -35,7 +35,7 @@
}, },
"dependencies": { "dependencies": {
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"lucide-vue-next": "^0.427.0", "lucide-vue-next": "^0.428.0",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -56,7 +56,7 @@
}, },
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^4.1.0", "@ctrl/tinycolor": "^4.1.0",
"@vue/shared": "^3.4.38", "@vue/shared": "^3.4.37",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"defu": "^6.1.4", "defu": "^6.1.4",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View File

@ -38,7 +38,7 @@
} }
}, },
"dependencies": { "dependencies": {
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -107,20 +107,23 @@ type MergeAll<
? MergeAll<Rest, Merge<R, F>> ? MergeAll<Rest, Merge<R, F>>
: R; : R;
export { type EmitType = (name: Name, ...args: any[]) => void;
type AnyFunction,
type AnyNormalFunction, export type {
type AnyPromiseFunction, AnyFunction,
type DeepPartial, AnyNormalFunction,
type DeepReadonly, AnyPromiseFunction,
type IntervalHandle, DeepPartial,
type MaybeComputedRef, DeepReadonly,
type MaybeReadonlyRef, EmitType,
type Merge, IntervalHandle,
type MergeAll, MaybeComputedRef,
type NonNullable, MaybeReadonlyRef,
type Nullable, Merge,
type ReadonlyRecordable, MergeAll,
type Recordable, NonNullable,
type TimeoutHandle, Nullable,
ReadonlyRecordable,
Recordable,
TimeoutHandle,
}; };

View File

@ -36,10 +36,10 @@
}, },
"dependencies": { "dependencies": {
"@vben-core/shared": "workspace:*", "@vben-core/shared": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"radix-vue": "^1.9.4", "radix-vue": "^1.9.4",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"vue": "^3.4.38" "vue": "^3.4.37"
}, },
"devDependencies": { "devDependencies": {
"@types/sortablejs": "^1.15.8" "@types/sortablejs": "^1.15.8"

View File

@ -1,5 +1,5 @@
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { computed, nextTick, onMounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT, CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
@ -14,6 +14,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
* @zh_CN content style * @zh_CN content style
*/ */
function useContentStyle() { function useContentStyle() {
let resizeObserver: null | ResizeObserver = null;
const contentElement = ref<HTMLDivElement | null>(null); const contentElement = ref<HTMLDivElement | null>(null);
const visibleDomRect = ref<null | VisibleDomRect>(null); const visibleDomRect = ref<null | VisibleDomRect>(null);
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT); const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
@ -41,12 +42,15 @@ function useContentStyle() {
); );
onMounted(() => { onMounted(() => {
nextTick(() => { if (contentElement.value && !resizeObserver) {
if (contentElement.value) { resizeObserver = new ResizeObserver(debouncedCalcHeight);
const observer = new ResizeObserver(debouncedCalcHeight); resizeObserver.observe(contentElement.value);
observer.observe(contentElement.value); }
} });
});
onUnmounted(() => {
resizeObserver?.disconnect();
resizeObserver = null;
}); });
return { contentElement, overlayStyle, visibleDomRect }; return { contentElement, overlayStyle, visibleDomRect };

View File

@ -39,7 +39,7 @@ describe('useSortable', () => {
expect(Sortable.default.create).toHaveBeenCalledWith( expect(Sortable.default.create).toHaveBeenCalledWith(
mockElement, mockElement,
expect.objectContaining({ expect.objectContaining({
animation: 100, animation: 300,
delay: 400, delay: 400,
delayOnTouchOnly: true, delayOnTouchOnly: true,
...customOptions, ...customOptions,

View File

@ -18,7 +18,7 @@ function useSortable<T extends HTMLElement>(
// Sortable?.default?.mount?.(AutoScroll); // Sortable?.default?.mount?.(AutoScroll);
const sortable = Sortable?.default?.create?.(sortableContainer, { const sortable = Sortable?.default?.create?.(sortableContainer, {
animation: 100, animation: 300,
delay: 400, delay: 400,
delayOnTouchOnly: true, delayOnTouchOnly: true,
...options, ...options,

View File

@ -31,7 +31,7 @@
"dependencies": { "dependencies": {
"@vben-core/shared": "workspace:*", "@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -41,7 +41,7 @@
"@vben-core/icons": "workspace:*", "@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -42,7 +42,7 @@
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*", "@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -46,10 +46,10 @@
"@vben-core/icons": "workspace:*", "@vben-core/icons": "workspace:*",
"@vben-core/shared": "workspace:*", "@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"lucide-vue-next": "^0.427.0", "lucide-vue-next": "^0.428.0",
"radix-vue": "^1.9.4", "radix-vue": "^1.9.4",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared'; import { cn } from '@vben-core/shared';
@ -11,6 +11,10 @@ interface Props {
scrollBarClass?: any; scrollBarClass?: any;
shadow?: boolean; shadow?: boolean;
shadowBorder?: boolean; shadowBorder?: boolean;
shadowBottom?: boolean;
shadowLeft?: boolean;
shadowRight?: boolean;
shadowTop?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -18,29 +22,66 @@ const props = withDefaults(defineProps<Props>(), {
horizontal: false, horizontal: false,
shadow: false, shadow: false,
shadowBorder: false, shadowBorder: false,
shadowBottom: true,
shadowLeft: false,
shadowRight: false,
shadowTop: true,
}); });
const emit = defineEmits<{
scrollAt: [{ bottom: boolean; left: boolean; right: boolean; top: boolean }];
}>();
const isAtTop = ref(true); const isAtTop = ref(true);
const isAtRight = ref(false);
const isAtBottom = ref(false); const isAtBottom = ref(false);
const isAtLeft = ref(true);
const showShadowTop = computed(() => props.shadow && props.shadowTop);
const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
const showShadowRight = computed(() => props.shadow && props.shadowRight);
const computedShadowClasses = computed(() => ({
'shadow-both':
!isAtLeft.value &&
!isAtRight.value &&
showShadowLeft.value &&
showShadowRight.value,
'shadow-left': !isAtLeft.value && showShadowLeft.value,
'shadow-right': !isAtRight.value && showShadowRight.value,
}));
function handleScroll(event: Event) { function handleScroll(event: Event) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const scrollTop = target?.scrollTop ?? 0; const scrollTop = target?.scrollTop ?? 0;
const scrollLeft = target?.scrollLeft ?? 0;
const offsetHeight = target?.offsetHeight ?? 0; const offsetHeight = target?.offsetHeight ?? 0;
const offsetWidth = target?.offsetWidth ?? 0;
const scrollHeight = target?.scrollHeight ?? 0; const scrollHeight = target?.scrollHeight ?? 0;
const scrollWidth = target?.scrollWidth ?? 0;
isAtTop.value = scrollTop <= 0; isAtTop.value = scrollTop <= 0;
isAtLeft.value = scrollLeft <= 0;
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight; isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
emit('scrollAt', {
bottom: isAtBottom.value,
left: isAtLeft.value,
right: isAtRight.value,
top: isAtTop.value,
});
} }
</script> </script>
<template> <template>
<ScrollArea <ScrollArea
:class="[cn(props.class)]" :class="[cn(props.class), computedShadowClasses]"
:on-scroll="handleScroll" :on-scroll="handleScroll"
class="relative" class="vben-scrollbar relative"
> >
<div <div
v-if="shadow" v-if="showShadowTop"
:class="{ :class="{
'opacity-100': !isAtTop, 'opacity-100': !isAtTop,
'border-border border-t': shadowBorder && !isAtTop, 'border-border border-t': shadowBorder && !isAtTop,
@ -49,7 +90,7 @@ function handleScroll(event: Event) {
></div> ></div>
<slot></slot> <slot></slot>
<div <div
v-if="shadow" v-if="showShadowBottom"
:class="{ :class="{
'opacity-100': !isAtTop && !isAtBottom, 'opacity-100': !isAtTop && !isAtBottom,
'border-border border-b': shadowBorder && !isAtTop && !isAtBottom, 'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,
@ -65,6 +106,31 @@ function handleScroll(event: Event) {
</template> </template>
<style scoped> <style scoped>
.vben-scrollbar {
&:not(.shadow-both).shadow-left {
mask-image: linear-gradient(90deg, transparent, #000 16px);
}
&:not(.shadow-both).shadow-right {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
&.shadow-both {
mask-image: linear-gradient(
90deg,
transparent,
#000 16px,
#000 calc(100% - 16px),
transparent 100%
);
}
}
.scrollbar-top-shadow { .scrollbar-top-shadow {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,

View File

@ -41,6 +41,7 @@
"@vben-core/icons": "workspace:*", "@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"vue": "^3.4.38" "@vueuse/core": "^11.0.0",
"vue": "^3.4.37"
} }
} }

View File

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

View File

@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types'; import type { TabConfig, TabsProps } from '../../types';
import { computed, watch } from 'vue'; import { computed } from 'vue';
import { MdiPin, X } from '@vben-core/icons'; 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 {} interface Props extends TabsProps {}
@ -21,7 +21,10 @@ const props = withDefaults(defineProps<Props>(), {
tabs: () => [], tabs: () => [],
}); });
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>(); const emit = defineEmits<{
close: [string];
unpin: [TabDefinition];
}>();
const active = defineModel<string>('active'); const active = defineModel<string>('active');
const typeWithClass = computed(() => { 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> </script>
<template> <template>
<div class="size-full flex-1 overflow-hidden"> <div
<VbenScrollbar :class="contentClass"
id="tabs-scrollbar" class="relative !flex h-full w-max items-center pr-6"
class="tabs-scrollbar h-full" >
horizontal <TransitionGroup name="slide-left">
scroll-bar-class="z-10 hidden"
>
<div <div
:class="contentClass" v-for="(tab, i) in tabsView"
class="relative !flex h-full w-max items-center" :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"> <VbenContextMenu
<div :handler-data="tab"
v-for="(tab, i) in tabsView" :menus="contextMenus"
:key="tab.key" :modal="false"
:class="[ item-class="pr-6"
{ >
'is-active dark:bg-accent bg-primary/15': tab.key === active, <div class="relative flex size-full items-center">
dragable: !tab.affixTab, <!-- extra -->
}, <div
typeWithClass.content, class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden"
]"
: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"
> >
<div class="relative flex size-full items-center"> <!-- close-icon -->
<!-- extra --> <X
<div v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden" 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)"
<!-- close-icon --> />
<X <MdiPin
v-show=" v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
!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)"
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)" </div>
/>
<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 --> <!-- tab-item-main -->
<div <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" 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 <VbenIcon
v-if="showIcon" v-if="showIcon"
:icon="tab.icon" :icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden" class="mr-2 flex size-4 items-center overflow-hidden"
fallback fallback
/> />
<span <span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
class="flex-1 overflow-hidden whitespace-nowrap text-sm" {{ tab.title }}
> </span>
{{ tab.title }} </div>
</span>
</div>
</div>
</VbenContextMenu>
</div> </div>
</TransitionGroup> </VbenContextMenu>
</div> </div>
</VbenScrollbar> </TransitionGroup>
</div> </div>
</template> </template>
<style scoped>
.tabs-scrollbar {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
</style>

View File

@ -1,15 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Sortable } from '@vben-core/composables'; import type { TabsEmits, TabsProps } from './types';
import type { TabDefinition } from '@vben-core/typings';
import type { TabsProps } from './types'; import { useForwardPropsEmits } from '@vben-core/composables';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
import { ChevronLeft, ChevronRight } from '@vben-core/icons'; import { ChevronLeft, ChevronRight } from '@vben-core/icons';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { Tabs, TabsChrome } from './components'; import { Tabs, TabsChrome } from './components';
import { useTabsDrag } from './use-tabs-drag';
import { useTabsViewScroll } from './use-tabs-view-scroll'; import { useTabsViewScroll } from './use-tabs-view-scroll';
interface Props extends TabsProps {} interface Props extends TabsProps {}
@ -24,136 +21,69 @@ const props = withDefaults(defineProps<Props>(), {
styleType: 'chrome', styleType: 'chrome',
}); });
const emit = defineEmits<{ const emit = defineEmits<TabsEmits>();
close: [string];
sortTabs: [number, number];
unpin: [TabDefinition];
}>();
const forward = useForwardPropsEmits(props, emit); const forward = useForwardPropsEmits(props, emit);
const { initScrollbar, scrollDirection } = useTabsViewScroll(); const {
handleScrollAt,
scrollbarRef,
scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
showScrollButton,
} = useTabsViewScroll(props);
const sortableInstance = ref<null | Sortable>(null); useTabsDrag(props, emit);
// domtab
function findParentElement(element: HTMLElement) {
const parentCls = 'group';
return element.classList.contains(parentCls)
? element
: element.closest(`.${parentCls}`);
}
async function initTabsSortable() {
await nextTick();
const { contentClass } = props;
const el = document.querySelectorAll(`.${contentClass}`)?.[0] as HTMLElement;
const resetElState = () => {
el.style.cursor = 'default';
el.classList.remove('dragging');
};
const { initializeSortable } = useSortable(el, {
filter: (_evt, target: HTMLElement) => {
const parent = findParentElement(target);
const dragable = parent?.classList.contains('dragable');
return !dragable || !props.dragable;
},
onEnd(evt) {
const { newIndex, oldIndex } = evt;
// const fromElement = evt.item;
const { srcElement } = (evt as any).originalEvent;
if (!srcElement) {
resetElState();
return;
}
const srcParent = findParentElement(srcElement);
if (!srcParent) {
resetElState();
return;
}
if (!srcParent.classList.contains('dragable')) {
resetElState();
return;
}
if (
oldIndex !== undefined &&
newIndex !== undefined &&
!Number.isNaN(oldIndex) &&
!Number.isNaN(newIndex) &&
oldIndex !== newIndex
) {
emit('sortTabs', oldIndex, newIndex);
}
resetElState();
},
onMove(evt) {
const parent = findParentElement(evt.related);
return parent?.classList.contains('dragable') && props.dragable;
},
onStart: () => {
el.style.cursor = 'grabbing';
el.classList.add('dragging');
},
});
sortableInstance.value = await initializeSortable();
}
async function init() {
await nextTick();
initTabsSortable();
initScrollbar();
}
onMounted(() => {
init();
});
watch(
() => props.styleType,
() => {
sortableInstance.value?.destroy();
init();
},
);
onUnmounted(() => {
sortableInstance.value?.destroy();
});
</script> </script>
<template> <template>
<div <div class="flex h-full flex-1 overflow-hidden">
:class="{
'overflow-hidden': styleType !== 'chrome',
}"
class="flex h-full flex-1"
>
<!-- 左侧滚动按钮 --> <!-- 左侧滚动按钮 -->
<span <span
class="hover:bg-muted text-muted-foreground cursor-pointer border-r px-2" v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,
'pointer-events-none opacity-30': scrollIsAtLeft,
}"
class="border-r px-2"
@click="scrollDirection('left')" @click="scrollDirection('left')"
> >
<ChevronLeft class="size-4 h-full" /> <ChevronLeft class="size-4 h-full" />
</span> </span>
<TabsChrome <div
v-if="styleType === 'chrome'" :class="{
v-bind="{ ...forward, ...$attrs, ...$props }" 'pt-[3px]': styleType === 'chrome',
/> }"
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" /> class="size-full flex-1 overflow-hidden"
>
<VbenScrollbar
ref="scrollbarRef"
class="h-full"
horizontal
scroll-bar-class="z-10 hidden"
shadow
shadow-left
shadow-right
@scroll-at="handleScrollAt"
>
<TabsChrome
v-if="styleType === 'chrome'"
v-bind="{ ...forward, ...$attrs, ...$props }"
/>
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
</VbenScrollbar>
</div>
<!-- 左侧滚动按钮 --> <!-- 左侧滚动按钮 -->
<span <span
v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,
'pointer-events-none opacity-30': scrollIsAtRight,
}"
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2" class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
@click="scrollDirection('right')" @click="scrollDirection('right')"
> >

View File

@ -1,7 +1,14 @@
import type { IContextMenuItem } from '@vben-core/shadcn-ui'; import type { IContextMenuItem } from '@vben-core/shadcn-ui';
import type { TabDefinition, TabsStyleType } from '@vben-core/typings'; import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
interface TabsProps { export type TabsEmits = {
close: [string];
sortTabs: [number, number];
unpin: [TabDefinition];
};
export interface TabsProps {
active?: string;
/** /**
* @zh_CN content class * @zh_CN content class
* @default tabs-chrome * @default tabs-chrome
@ -48,12 +55,10 @@ interface TabsProps {
tabs?: TabDefinition[]; tabs?: TabDefinition[];
} }
interface TabConfig extends TabDefinition { export interface TabConfig extends TabDefinition {
affixTab: boolean; affixTab: boolean;
closable: boolean; closable: boolean;
icon: string; icon: string;
key: string; key: string;
title: string; title: string;
} }
export type { TabConfig, TabsProps };

View File

@ -0,0 +1,110 @@
import type { EmitType } from '@vben-core/typings';
import type { TabsProps } from './types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { type Sortable, useSortable } from '@vben-core/composables';
// 可能会找到拖拽的子元素这里需要确保拖拽的dom时tab元素
function findParentElement(element: HTMLElement) {
const parentCls = 'group';
return element.classList.contains(parentCls)
? element
: element.closest(`.${parentCls}`);
}
export function useTabsDrag(props: TabsProps, emit: EmitType) {
const sortableInstance = ref<null | Sortable>(null);
async function initTabsSortable() {
await nextTick();
const el = document.querySelectorAll(
`.${props.contentClass}`,
)?.[0] as HTMLElement;
if (!el) {
console.warn('Element not found for sortable initialization');
return;
}
const resetElState = async () => {
el.style.cursor = 'default';
el.classList.remove('dragging');
el.querySelector('.draggable')?.classList.remove('dragging');
};
const { initializeSortable } = useSortable(el, {
filter: (_evt, target: HTMLElement) => {
const parent = findParentElement(target);
const dragable = parent?.classList.contains('dragable');
return !dragable || !props.dragable;
},
onEnd(evt) {
const { newIndex, oldIndex } = evt;
// const fromElement = evt.item;
const { srcElement } = (evt as any).originalEvent;
if (!srcElement) {
resetElState();
return;
}
const srcParent = findParentElement(srcElement);
if (!srcParent) {
resetElState();
return;
}
if (!srcParent.classList.contains('dragable')) {
resetElState();
return;
}
if (
oldIndex !== undefined &&
newIndex !== undefined &&
!Number.isNaN(oldIndex) &&
!Number.isNaN(newIndex) &&
oldIndex !== newIndex
) {
emit('sortTabs', oldIndex, newIndex);
}
resetElState();
},
onMove(evt) {
const parent = findParentElement(evt.related);
return parent?.classList.contains('dragable') && props.dragable;
},
onStart: () => {
el.style.cursor = 'grabbing';
el.querySelector('.draggable')?.classList.add('dragging');
// el.classList.add('dragging');
},
});
sortableInstance.value = await initializeSortable();
}
async function init() {
await nextTick();
initTabsSortable();
}
onMounted(init);
watch(
() => props.styleType,
() => {
sortableInstance.value?.destroy();
init();
},
);
onUnmounted(() => {
sortableInstance.value?.destroy();
});
}

View File

@ -1,15 +1,28 @@
import { nextTick, ref } from 'vue'; import type { TabsProps } from './types';
type El = Element | null | undefined; import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
export function useTabsViewScroll(scrollDistance: number = 150) { import { VbenScrollbar } from '@vben-core/shadcn-ui';
const scrollbarEl = ref<El>(null);
const scrollViewportEl = ref<El>(null); import { useDebounceFn } from '@vueuse/core';
type DomElement = Element | null | undefined;
export function useTabsViewScroll(props: TabsProps) {
let resizeObserver: null | ResizeObserver = null;
let mutationObserver: MutationObserver | null = null;
let tabItemCount = 0;
const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);
const scrollViewportEl = ref<DomElement>(null);
const showScrollButton = ref(false);
const scrollIsAtLeft = ref(true);
const scrollIsAtRight = ref(false);
function getScrollClientWidth() { function getScrollClientWidth() {
if (!scrollbarEl.value || !scrollViewportEl.value) return {}; const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl || !scrollViewportEl.value) return {};
const scrollbarWidth = scrollbarEl.value.clientWidth; const scrollbarWidth = scrollbarEl.clientWidth;
const scrollViewWidth = scrollViewportEl.value.clientWidth; const scrollViewWidth = scrollViewportEl.value.clientWidth;
return { return {
@ -20,7 +33,7 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
function scrollDirection( function scrollDirection(
direction: 'left' | 'right', direction: 'left' | 'right',
distance: number = scrollDistance, distance: number = 150,
) { ) {
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth(); const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
@ -39,21 +52,142 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
async function initScrollbar() { async function initScrollbar() {
await nextTick(); await nextTick();
const barEl = document.querySelector('#tabs-scrollbar');
const viewportEl = barEl?.querySelector( const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl) {
return;
}
const viewportEl = scrollbarEl?.querySelector(
'div[data-radix-scroll-area-viewport]', 'div[data-radix-scroll-area-viewport]',
); );
scrollbarEl.value = barEl;
scrollViewportEl.value = viewportEl; scrollViewportEl.value = viewportEl;
calcShowScrollbarButton();
const activeItem = viewportEl?.querySelector('.is-active'); await nextTick();
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'start' }); scrollToActiveIntoView();
// 监听大小变化
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(
useDebounceFn((_entries: ResizeObserverEntry[]) => {
calcShowScrollbarButton();
}, 100),
);
resizeObserver.observe(viewportEl);
tabItemCount = props.tabs?.length || 0;
mutationObserver?.disconnect();
// 使用 MutationObserver 仅监听子节点数量变化
mutationObserver = new MutationObserver(() => {
const count = viewportEl.querySelectorAll(
`div[data-tab-item="true"]`,
).length;
if (count > tabItemCount) {
scrollToActiveIntoView();
}
if (count !== tabItemCount) {
calcShowScrollbarButton();
tabItemCount = count;
}
});
// 配置为仅监听子节点的添加和移除
mutationObserver.observe(viewportEl, {
attributes: false,
childList: true,
subtree: true,
});
} }
async function scrollToActiveIntoView() {
if (!scrollViewportEl.value) {
return;
}
await nextTick();
const viewportEl = scrollViewportEl.value;
const { scrollbarWidth } = getScrollClientWidth();
const { scrollWidth } = viewportEl;
if (scrollbarWidth >= scrollWidth) {
return;
}
requestAnimationFrame(() => {
const activeItem = viewportEl?.querySelector('.is-active');
activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
});
}
/**
* tabs
*/
async function calcShowScrollbarButton() {
if (!scrollViewportEl.value) {
return;
}
const { scrollbarWidth } = getScrollClientWidth();
showScrollButton.value =
scrollViewportEl.value.scrollWidth > scrollbarWidth;
}
const handleScrollAt = useDebounceFn(({ left, right }) => {
scrollIsAtLeft.value = left;
scrollIsAtRight.value = right;
}, 100);
watch(
() => props.active,
async () => {
// 200为了等待 tab 切换动画完成
// setTimeout(() => {
scrollToActiveIntoView();
// }, 300);
},
{
flush: 'post',
},
);
// watch(
// () => props.tabs?.length,
// async () => {
// await nextTick();
// calcShowScrollbarButton();
// },
// {
// flush: 'post',
// },
// );
watch(
() => props.styleType,
() => {
initScrollbar();
},
);
onMounted(initScrollbar);
onUnmounted(() => {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
resizeObserver = null;
mutationObserver = null;
});
return { return {
handleScrollAt,
initScrollbar, initScrollbar,
scrollbarRef,
scrollDirection, scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
showScrollButton,
}; };
} }

View File

@ -24,6 +24,6 @@
"@vben/stores": "workspace:*", "@vben/stores": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -21,8 +21,8 @@
}, },
"dependencies": { "dependencies": {
"@vben/preferences": "workspace:*", "@vben/preferences": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"vue": "^3.4.38" "vue": "^3.4.37"
} }
} }

View File

@ -26,9 +26,9 @@
"@vben/icons": "workspace:*", "@vben/icons": "workspace:*",
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vueuse/integrations": "^10.11.1", "@vueuse/integrations": "^11.0.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -25,7 +25,7 @@
"@vben/stores": "workspace:*", "@vben/stores": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",
"watermark-js-plus": "^1.5.3" "watermark-js-plus": "^1.5.3"
} }

View File

@ -32,8 +32,8 @@
"@vben/stores": "workspace:*", "@vben/stores": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -21,7 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@intlify/core-base": "^9.13.1", "@intlify/core-base": "^9.13.1",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-i18n": "^9.13.1" "vue-i18n": "^9.13.1"
} }
} }

View File

@ -24,7 +24,7 @@
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"pinia": "2.2.2", "pinia": "2.2.2",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -124,10 +124,21 @@ export const useTabbarStore = defineStore('core-tabbar', {
} else { } else {
// 页面已经存在,不重复添加选项卡,只更新选项卡参数 // 页面已经存在,不重复添加选项卡,只更新选项卡参数
const currentTab = toRaw(this.tabs)[tabIndex]; const currentTab = toRaw(this.tabs)[tabIndex];
const mergedTab = { ...currentTab, ...tab }; const mergedTab = {
if (currentTab && Reflect.has(currentTab.meta, 'affixTab')) { ...currentTab,
mergedTab.meta.affixTab = currentTab.meta.affixTab; ...tab,
meta: { ...currentTab?.meta, ...tab.meta },
};
if (currentTab) {
const curMeta = currentTab.meta;
if (Reflect.has(curMeta, 'affixTab')) {
mergedTab.meta.affixTab = curMeta.affixTab;
}
if (Reflect.has(curMeta, 'newTabTitle')) {
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
}
} }
this.tabs.splice(tabIndex, 1, mergedTab); this.tabs.splice(tabIndex, 1, mergedTab);
} }
this.updateCacheTab(); this.updateCacheTab();

View File

@ -21,7 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.3", "ant-design-vue": "^4.2.3",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"pinia": "2.2.2", "pinia": "2.2.2",
"vue": "^3.4.38", "vue": "^3.4.37",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
} }
} }

View File

@ -161,7 +161,7 @@ const routes: RouteRecordRaw[] = [
import( import(
'#/views/demos/features/hide-menu-children/children.vue' '#/views/demos/features/hide-menu-children/children.vue'
), ),
meta: { title: 'HideChildrenInMenuChildrenDemo' }, meta: { title: $t('page.demos.features.hideChildrenInMenu') },
}, },
], ],
}, },

View File

@ -165,8 +165,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/utils version: link:../../packages/utils
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
ant-design-vue: ant-design-vue:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3(vue@3.4.38(typescript@5.5.4)) version: 4.2.3(vue@3.4.38(typescript@5.5.4))
@ -228,8 +228,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/utils version: link:../../packages/utils
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
dayjs: dayjs:
specifier: ^1.11.12 specifier: ^1.11.12
version: 1.11.12 version: 1.11.12
@ -295,8 +295,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/utils version: link:../../packages/utils
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
naive-ui: naive-ui:
specifier: ^2.39.0 specifier: ^2.39.0
version: 2.39.0(vue@3.4.38(typescript@5.5.4)) version: 2.39.0(vue@3.4.38(typescript@5.5.4))
@ -660,8 +660,8 @@ importers:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2(vue@3.4.38(typescript@5.5.4)) version: 4.1.2(vue@3.4.38(typescript@5.5.4))
lucide-vue-next: lucide-vue-next:
specifier: ^0.427.0 specifier: ^0.428.0
version: 0.427.0(vue@3.4.38(typescript@5.5.4)) version: 0.428.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -672,7 +672,7 @@ importers:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
'@vue/shared': '@vue/shared':
specifier: ^3.4.38 specifier: ^3.4.37
version: 3.4.38 version: 3.4.38
clsx: clsx:
specifier: 2.1.1 specifier: 2.1.1
@ -715,8 +715,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../base/shared version: link:../base/shared
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
radix-vue: radix-vue:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4(vue@3.4.38(typescript@5.5.4)) version: 1.9.4(vue@3.4.38(typescript@5.5.4))
@ -740,8 +740,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../base/typings version: link:../base/typings
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -761,8 +761,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../base/typings version: link:../../base/typings
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -785,8 +785,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../base/typings version: link:../../base/typings
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -806,14 +806,14 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../base/typings version: link:../../base/typings
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
lucide-vue-next: lucide-vue-next:
specifier: ^0.427.0 specifier: ^0.428.0
version: 0.427.0(vue@3.4.38(typescript@5.5.4)) version: 0.428.0(vue@3.4.38(typescript@5.5.4))
radix-vue: radix-vue:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4(vue@3.4.38(typescript@5.5.4)) version: 1.9.4(vue@3.4.38(typescript@5.5.4))
@ -835,6 +835,9 @@ importers:
'@vben-core/typings': '@vben-core/typings':
specifier: workspace:* specifier: workspace:*
version: link:../../base/typings version: link:../../base/typings
'@vueuse/core':
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -869,8 +872,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../preferences version: link:../../preferences
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
echarts: echarts:
specifier: ^5.5.1 specifier: ^5.5.1
version: 5.5.1 version: 5.5.1
@ -899,8 +902,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../types version: link:../../types
'@vueuse/integrations': '@vueuse/integrations':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))
qrcode: qrcode:
specifier: ^1.5.4 specifier: ^1.5.4
version: 1.5.4 version: 1.5.4
@ -981,8 +984,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../utils version: link:../../utils
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue: vue:
specifier: 3.4.38 specifier: 3.4.38
version: 3.4.38(typescript@5.5.4) version: 3.4.38(typescript@5.5.4)
@ -1129,8 +1132,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../packages/utils version: link:../packages/utils
'@vueuse/core': '@vueuse/core':
specifier: ^10.11.1 specifier: ^11.0.0
version: 10.11.1(vue@3.4.38(typescript@5.5.4)) version: 11.0.0(vue@3.4.38(typescript@5.5.4))
ant-design-vue: ant-design-vue:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3(vue@3.4.38(typescript@5.5.4)) version: 4.2.3(vue@3.4.38(typescript@5.5.4))
@ -4179,6 +4182,9 @@ packages:
'@vueuse/core@10.11.1': '@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@11.0.0':
resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==}
'@vueuse/core@9.13.0': '@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
@ -4223,21 +4229,21 @@ packages:
universal-cookie: universal-cookie:
optional: true optional: true
'@vueuse/integrations@10.11.1': '@vueuse/integrations@11.0.0':
resolution: {integrity: sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==} resolution: {integrity: sha512-B95nBX4B2q2ZETBDldrKARM/fYXBHfwdo44UbHBq4bUTi25lrlc8MwAZGqEoRvdV4ND9T6O1Rb9e4kaCJFXnqw==}
peerDependencies: peerDependencies:
async-validator: ^4 async-validator: ^4
axios: ^1 axios: ^1
change-case: ^4 change-case: ^5
drauu: ^0.3 drauu: ^0.4
focus-trap: ^7 focus-trap: ^7
fuse.js: ^6 fuse.js: ^7
idb-keyval: ^6 idb-keyval: ^6
jwt-decode: ^3 jwt-decode: ^4
nprogress: ^0.2 nprogress: ^0.2
qrcode: ^1.5 qrcode: ^1.5
sortablejs: ^1 sortablejs: ^1
universal-cookie: ^6 universal-cookie: ^7
peerDependenciesMeta: peerDependenciesMeta:
async-validator: async-validator:
optional: true optional: true
@ -4270,6 +4276,9 @@ packages:
'@vueuse/metadata@10.11.1': '@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@11.0.0':
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
'@vueuse/metadata@9.13.0': '@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
@ -4279,6 +4288,9 @@ packages:
'@vueuse/shared@10.11.1': '@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@11.0.0':
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
'@vueuse/shared@9.13.0': '@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@ -6916,8 +6928,8 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
lucide-vue-next@0.427.0: lucide-vue-next@0.428.0:
resolution: {integrity: sha512-zI1FhbfQ3Wl0SgPKnOWhTDC6yAC5TTjSC9FSZ61ULg3U36e+GVK+RT1qfkU9Q5BjeBuwmsHWKsXKptKMjUAwFA==} resolution: {integrity: sha512-of9GJGus9VKGIUOp3yQ0uQtNv+8MRLaso8H4OiDzI6+T7TeMRXTzqVOLhnyg9fdXUnYuwE9Xm1zD1nfQ7oFPmg==}
peerDependencies: peerDependencies:
vue: 3.4.38 vue: 3.4.38
@ -13196,6 +13208,16 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/core@11.0.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 11.0.0
'@vueuse/shared': 11.0.0(vue@3.4.38(typescript@5.5.4))
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/core@9.13.0(vue@3.4.38(typescript@5.5.4))': '@vueuse/core@9.13.0(vue@3.4.38(typescript@5.5.4))':
dependencies: dependencies:
'@types/web-bluetooth': 0.0.16 '@types/web-bluetooth': 0.0.16
@ -13222,10 +13244,10 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/integrations@10.11.1(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))': '@vueuse/integrations@11.0.0(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))':
dependencies: dependencies:
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4)) '@vueuse/core': 11.0.0(vue@3.4.38(typescript@5.5.4))
'@vueuse/shared': 10.11.1(vue@3.4.38(typescript@5.5.4)) '@vueuse/shared': 11.0.0(vue@3.4.38(typescript@5.5.4))
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
optionalDependencies: optionalDependencies:
async-validator: 4.2.5 async-validator: 4.2.5
@ -13242,6 +13264,8 @@ snapshots:
'@vueuse/metadata@10.11.1': {} '@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@11.0.0': {}
'@vueuse/metadata@9.13.0': {} '@vueuse/metadata@9.13.0': {}
'@vueuse/shared@10.11.0(vue@3.4.38(typescript@5.5.4))': '@vueuse/shared@10.11.0(vue@3.4.38(typescript@5.5.4))':
@ -13258,6 +13282,13 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/shared@11.0.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@9.13.0(vue@3.4.38(typescript@5.5.4))': '@vueuse/shared@9.13.0(vue@3.4.38(typescript@5.5.4))':
dependencies: dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
@ -16250,7 +16281,7 @@ snapshots:
dependencies: dependencies:
yallist: 4.0.0 yallist: 4.0.0
lucide-vue-next@0.427.0(vue@3.4.38(typescript@5.5.4)): lucide-vue-next@0.428.0(vue@3.4.38(typescript@5.5.4)):
dependencies: dependencies:
vue: 3.4.38(typescript@5.5.4) vue: 3.4.38(typescript@5.5.4)