feat: tabs adds a variety of style configurations
parent
ebf73b2df9
commit
3a91a24e0d
|
@ -69,10 +69,12 @@ const defaultPreferences: Preferences = {
|
||||||
width: 240,
|
width: 240,
|
||||||
},
|
},
|
||||||
tabbar: {
|
tabbar: {
|
||||||
|
dragable: true,
|
||||||
enable: true,
|
enable: true,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
persist: true,
|
persist: true,
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
|
styleType: 'chrome',
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
builtinType: 'default',
|
builtinType: 'default',
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
NavigationStyleType,
|
NavigationStyleType,
|
||||||
PageTransitionType,
|
PageTransitionType,
|
||||||
SupportedLanguagesType,
|
SupportedLanguagesType,
|
||||||
|
TabsStyleType,
|
||||||
ThemeModeType,
|
ThemeModeType,
|
||||||
} from '@vben-core/typings';
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
|
@ -135,6 +136,8 @@ interface ShortcutKeyPreferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabbarPreferences {
|
interface TabbarPreferences {
|
||||||
|
/** 是否开启多标签页拖拽 */
|
||||||
|
dragable: boolean;
|
||||||
/** 是否开启多标签页 */
|
/** 是否开启多标签页 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 开启标签页缓存功能 */
|
/** 开启标签页缓存功能 */
|
||||||
|
@ -143,6 +146,8 @@ interface TabbarPreferences {
|
||||||
persist: boolean;
|
persist: boolean;
|
||||||
/** 是否开启多标签页图标 */
|
/** 是否开启多标签页图标 */
|
||||||
showIcon: boolean;
|
showIcon: boolean;
|
||||||
|
/** 标签页风格 */
|
||||||
|
styleType: TabsStyleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThemePreferences {
|
interface ThemePreferences {
|
||||||
|
|
|
@ -176,6 +176,14 @@
|
||||||
"enable": "Enable Tab Bar",
|
"enable": "Enable Tab Bar",
|
||||||
"icon": "Show Tabbar Icon",
|
"icon": "Show Tabbar Icon",
|
||||||
"persist": "Persist Tabs",
|
"persist": "Persist Tabs",
|
||||||
|
"dragable": "Enable Dragable Sort",
|
||||||
|
"styleType": {
|
||||||
|
"title": "Tabs Style",
|
||||||
|
"chrome": "Chrome",
|
||||||
|
"card": "Card",
|
||||||
|
"plain": "Plain",
|
||||||
|
"brisk": "Brisk"
|
||||||
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
|
|
@ -176,6 +176,14 @@
|
||||||
"enable": "启用标签栏",
|
"enable": "启用标签栏",
|
||||||
"icon": "显示标签栏图标",
|
"icon": "显示标签栏图标",
|
||||||
"persist": "持久化标签页",
|
"persist": "持久化标签页",
|
||||||
|
"dragable": "启动拖拽排序",
|
||||||
|
"styleType": {
|
||||||
|
"title": "标签页风格",
|
||||||
|
"chrome": "谷歌",
|
||||||
|
"card": "卡片",
|
||||||
|
"plain": "朴素",
|
||||||
|
"brisk": "轻快"
|
||||||
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"reload": "重新加载",
|
"reload": "重新加载",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { SortableOptions } from 'sortablejs';
|
import type { SortableOptions } from 'sortablejs';
|
||||||
|
import type Sortable from 'sortablejs';
|
||||||
|
|
||||||
function useSortable<T extends HTMLElement>(
|
function useSortable<T extends HTMLElement>(
|
||||||
sortableContainer: T,
|
sortableContainer: T,
|
||||||
|
@ -22,7 +23,7 @@ function useSortable<T extends HTMLElement>(
|
||||||
delayOnTouchOnly: true,
|
delayOnTouchOnly: true,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
return sortable;
|
return sortable as Sortable;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -31,3 +32,5 @@ function useSortable<T extends HTMLElement>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useSortable };
|
export { useSortable };
|
||||||
|
|
||||||
|
export type { Sortable };
|
||||||
|
|
|
@ -44,6 +44,8 @@ type AccessModeType = 'allow-all' | 'backend' | 'frontend';
|
||||||
|
|
||||||
type NavigationStyleType = 'plain' | 'rounded';
|
type NavigationStyleType = 'plain' | 'rounded';
|
||||||
|
|
||||||
|
type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';
|
||||||
|
|
||||||
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
|
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
|
||||||
|
|
||||||
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
|
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
|
||||||
|
@ -60,5 +62,6 @@ export type {
|
||||||
NavigationStyleType,
|
NavigationStyleType,
|
||||||
PageTransitionType,
|
PageTransitionType,
|
||||||
SupportedLanguagesType,
|
SupportedLanguagesType,
|
||||||
|
TabsStyleType,
|
||||||
ThemeModeType,
|
ThemeModeType,
|
||||||
};
|
};
|
||||||
|
|
|
@ -267,7 +267,7 @@ function handleMouseleave() {
|
||||||
<div v-if="slots.logo" :style="headerStyle">
|
<div v-if="slots.logo" :style="headerStyle">
|
||||||
<slot name="logo"></slot>
|
<slot name="logo"></slot>
|
||||||
</div>
|
</div>
|
||||||
<VbenScrollbar :style="contentStyle">
|
<VbenScrollbar :style="contentStyle" shadow>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</VbenScrollbar>
|
</VbenScrollbar>
|
||||||
|
|
||||||
|
@ -297,7 +297,7 @@ function handleMouseleave() {
|
||||||
<div v-if="!extraCollapse" :style="extraTitleStyle">
|
<div v-if="!extraCollapse" :style="extraTitleStyle">
|
||||||
<slot name="extra-title"></slot>
|
<slot name="extra-title"></slot>
|
||||||
</div>
|
</div>
|
||||||
<VbenScrollbar :style="extraContentStyle" class="py-4">
|
<VbenScrollbar :style="extraContentStyle" class="py-4" shadow>
|
||||||
<slot name="extra"></slot>
|
<slot name="extra"></slot>
|
||||||
</VbenScrollbar>
|
</VbenScrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,8 +39,5 @@ const style = computed((): CSSProperties => {
|
||||||
<template>
|
<template>
|
||||||
<section :style="style" class="border-border flex w-full">
|
<section :style="style" class="border-border flex w-full">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div class="flex items-center">
|
|
||||||
<slot name="toolbar"></slot>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,15 +2,22 @@
|
||||||
import type { HTMLAttributes } from 'vue';
|
import type { HTMLAttributes } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { ScrollArea } from '@vben-core/shadcn-ui/components/ui/scroll-area';
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
ScrollBar,
|
||||||
|
} from '@vben-core/shadcn-ui/components/ui/scroll-area';
|
||||||
import { cn } from '@vben-core/toolkit';
|
import { cn } from '@vben-core/toolkit';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: HTMLAttributes['class'];
|
class?: HTMLAttributes['class'];
|
||||||
|
horizontal?: boolean;
|
||||||
|
shadow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
class: '',
|
class: '',
|
||||||
|
horizontal: false,
|
||||||
|
shadow: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAtTop = ref(true);
|
const isAtTop = ref(true);
|
||||||
|
@ -33,6 +40,7 @@ function handleScroll(event: Event) {
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="shadow"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-100': !isAtTop,
|
'opacity-100': !isAtTop,
|
||||||
}"
|
}"
|
||||||
|
@ -40,11 +48,13 @@ function handleScroll(event: Event) {
|
||||||
></div>
|
></div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div
|
<div
|
||||||
|
v-if="shadow"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-100': !isAtTop && !isAtBottom,
|
'opacity-100': !isAtTop && !isAtBottom,
|
||||||
}"
|
}"
|
||||||
class="scrollbar-bottom-shadow pointer-events-none absolute bottom-0 z-10 h-16 w-full opacity-0 transition-opacity duration-1000 ease-in-out will-change-[opacity]"
|
class="scrollbar-bottom-shadow pointer-events-none absolute bottom-0 z-10 h-16 w-full opacity-0 transition-opacity duration-1000 ease-in-out will-change-[opacity]"
|
||||||
></div>
|
></div>
|
||||||
|
<ScrollBar v-if="horizontal" orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,13 @@ import type { TabConfig, TabsProps } from '../../types';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { MdiPin } from '@vben-core/iconify';
|
import { IcRoundClose, MdiPin } from '@vben-core/iconify';
|
||||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
interface Props extends TabsProps {}
|
interface Props extends TabsProps {}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'TabsChrome',
|
name: 'VbenTabsChrome',
|
||||||
// eslint-disable-next-line perfectionist/sort-objects
|
// eslint-disable-next-line perfectionist/sort-objects
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
@ -94,7 +94,10 @@ function handleUnpinTab(tab: TabConfig) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :style="style" class="tabs-chrome bg-accent size-full pt-1">
|
<div
|
||||||
|
:style="style"
|
||||||
|
class="tabs-chrome bg-accent size-full flex-1 overflow-hidden pt-1"
|
||||||
|
>
|
||||||
<!-- footer -> 4px -->
|
<!-- footer -> 4px -->
|
||||||
<div
|
<div
|
||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
|
@ -157,16 +160,11 @@ function handleUnpinTab(tab: TabConfig) {
|
||||||
class="tabs-chrome__extra absolute right-[calc(var(--gap)*2)] top-1/2 z-[3] size-4 translate-y-[-50%] opacity-0 transition-opacity group-hover:opacity-100"
|
class="tabs-chrome__extra absolute right-[calc(var(--gap)*2)] top-1/2 z-[3] size-4 translate-y-[-50%] opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<!-- close-icon -->
|
<!-- close-icon -->
|
||||||
<svg
|
<IcRoundClose
|
||||||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:bg-accent hover:stroke-accent-foreground size-full cursor-pointer rounded-full transition-all"
|
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
|
||||||
height="12"
|
|
||||||
stroke="#595959"
|
|
||||||
width="12"
|
|
||||||
@click.stop="handleClose(tab.key)"
|
@click.stop="handleClose(tab.key)"
|
||||||
>
|
/>
|
||||||
<path d="M 4 4 L 12 12 M 12 4 L 4 12" />
|
|
||||||
</svg>
|
|
||||||
<MdiPin
|
<MdiPin
|
||||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||||
|
@ -186,7 +184,7 @@ function handleUnpinTab(tab: TabConfig) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap"
|
class="tabs-chrome__label text-accent-foreground ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,11 +1,141 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
import type { TabConfig, TabsProps } from '../../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { IcRoundClose, MdiPin } from '@vben-core/iconify';
|
||||||
|
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||||
|
import { TabDefinition } from '@vben-core/typings';
|
||||||
|
|
||||||
|
interface Props extends TabsProps {}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'VbenTabs',
|
||||||
|
// eslint-disable-next-line perfectionist/sort-objects
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
contentClass: 'vben-tabs-content',
|
||||||
|
contextMenus: () => [],
|
||||||
|
tabs: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
|
||||||
|
const active = defineModel<string>('active');
|
||||||
|
|
||||||
|
const typeWithClass = computed(() => {
|
||||||
|
const typeClasses: Record<string, { content: string }> = {
|
||||||
|
brisk: {
|
||||||
|
content: `h-full after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-full after:h-[1.5px] after:bg-primary after:scale-x-0 after:transition-[transform] after:ease-out after:duration-300 hover:after:scale-x-100 after:origin-left [&.is-active]:after:scale-x-100`,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
content: 'h-[90%] rounded-md mr-1',
|
||||||
|
},
|
||||||
|
plain: {
|
||||||
|
content: 'h-full',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeClasses[props.styleType || 'plain'];
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabsView = computed((): TabConfig[] => {
|
||||||
|
return props.tabs.map((tab) => {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
affixTab: !!tab.meta?.affixTab,
|
||||||
|
closable: Reflect.has(tab.meta, 'tabClosable')
|
||||||
|
? !!tab.meta.tabClosable
|
||||||
|
: true,
|
||||||
|
icon: tab.meta.icon as string,
|
||||||
|
key: tab.fullPath || tab.path,
|
||||||
|
title: (tab.meta?.title || tab.name) as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClose(key: string) {
|
||||||
|
emit('close', key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUnpinTab(tab: TabConfig) {
|
||||||
|
emit('unpin', tab);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-accent size-full">
|
<div class="bg-accent h-full flex-1 overflow-hidden">
|
||||||
<VbenScrollbar>
|
<VbenScrollbar class="h-full" horizontal>
|
||||||
<slot></slot>
|
<div
|
||||||
|
:class="contentClass"
|
||||||
|
class="relative !flex h-full w-max items-center"
|
||||||
|
>
|
||||||
|
<TransitionGroup name="slide-down">
|
||||||
|
<div
|
||||||
|
v-for="(tab, i) in tabsView"
|
||||||
|
:key="tab.key"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'is-active bg-background': tab.key === active,
|
||||||
|
dragable: !tab.affixTab,
|
||||||
|
},
|
||||||
|
typeWithClass.content,
|
||||||
|
]"
|
||||||
|
:data-index="i"
|
||||||
|
class="[&: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">
|
||||||
|
<!-- extra -->
|
||||||
|
<div
|
||||||
|
class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden opacity-0 transition-opacity group-hover:opacity-100 group-[.is-active]:opacity-100"
|
||||||
|
>
|
||||||
|
<!-- close-icon -->
|
||||||
|
<IcRoundClose
|
||||||
|
v-show="
|
||||||
|
!tab.affixTab && tabsView.length > 1 && tab.closable
|
||||||
|
"
|
||||||
|
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground size-3 cursor-pointer rounded-full transition-all"
|
||||||
|
@click.stop="handleClose(tab.key)"
|
||||||
|
/>
|
||||||
|
<MdiPin
|
||||||
|
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
|
class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||||
|
@click.stop="handleUnpinTab(tab)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- tab-item-main -->
|
||||||
|
<div
|
||||||
|
class="mx-3 mr-3 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<!-- <div
|
||||||
|
class="mx-3 ml-3 mr-2 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] transition-all duration-300 group-hover:mr-2 group-hover:pr-4 group-[.is-active]:pr-4"
|
||||||
|
> -->
|
||||||
|
<VbenIcon
|
||||||
|
v-if="showIcon"
|
||||||
|
:icon="tab.icon"
|
||||||
|
class="mr-2 flex size-4 items-center overflow-hidden"
|
||||||
|
fallback
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="text-accent-foreground flex-1 overflow-hidden whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ tab.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VbenContextMenu>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
</VbenScrollbar>
|
</VbenScrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Sortable } from '@vben-core/hooks';
|
||||||
import type { TabDefinition } from '@vben-core/typings';
|
import type { TabDefinition } from '@vben-core/typings';
|
||||||
|
|
||||||
import { nextTick, onMounted } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useForwardPropsEmits, useSortable } from '@vben-core/hooks';
|
import { useForwardPropsEmits, useSortable } from '@vben-core/hooks';
|
||||||
|
|
||||||
import { TabsChrome } from './components';
|
import { Tabs, TabsChrome } from './components';
|
||||||
import { TabsProps } from './types';
|
import { TabsProps } from './types';
|
||||||
|
|
||||||
interface Props extends TabsProps {}
|
interface Props extends TabsProps {}
|
||||||
|
@ -17,6 +18,7 @@ defineOptions({
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
contentClass: 'vben-tabs-content',
|
contentClass: 'vben-tabs-content',
|
||||||
dragable: true,
|
dragable: true,
|
||||||
|
styleType: 'chrome',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -27,13 +29,15 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const forward = useForwardPropsEmits(props, emit);
|
const forward = useForwardPropsEmits(props, emit);
|
||||||
|
|
||||||
|
const sortableInstance = ref<Sortable | null>(null);
|
||||||
|
|
||||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
||||||
const findParentElement = (element: HTMLElement) => {
|
function findParentElement(element: HTMLElement) {
|
||||||
const parentCls = 'group';
|
const parentCls = 'group';
|
||||||
return element.classList.contains(parentCls)
|
return element.classList.contains(parentCls)
|
||||||
? element
|
? element
|
||||||
: element.closest(`.${parentCls}`);
|
: element.closest(`.${parentCls}`);
|
||||||
};
|
}
|
||||||
|
|
||||||
async function initTabsSortable() {
|
async function initTabsSortable() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
@ -80,7 +84,7 @@ async function initTabsSortable() {
|
||||||
},
|
},
|
||||||
onMove(evt) {
|
onMove(evt) {
|
||||||
const parent = findParentElement(evt.related);
|
const parent = findParentElement(evt.related);
|
||||||
return parent?.classList.contains('dragable');
|
return parent?.classList.contains('dragable') && props.dragable;
|
||||||
},
|
},
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
el.style.cursor = 'grabbing';
|
el.style.cursor = 'grabbing';
|
||||||
|
@ -88,12 +92,17 @@ async function initTabsSortable() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await initializeSortable();
|
sortableInstance.value = await initializeSortable();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initTabsSortable);
|
onMounted(initTabsSortable);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
sortableInstance.value?.destroy();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsChrome v-bind="forward" />
|
<TabsChrome v-if="styleType === 'chrome'" v-bind="forward" />
|
||||||
|
<Tabs v-else v-bind="forward" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||||
import type { TabDefinition } from '@vben-core/typings';
|
import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
|
||||||
|
|
||||||
interface TabsProps {
|
interface TabsProps {
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +21,6 @@ interface TabsProps {
|
||||||
* 仅限 tabs-chrome
|
* 仅限 tabs-chrome
|
||||||
*/
|
*/
|
||||||
gap?: number;
|
gap?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN tab 最大宽度
|
* @zh_CN tab 最大宽度
|
||||||
* 仅限 tabs-chrome
|
* 仅限 tabs-chrome
|
||||||
|
@ -33,10 +32,15 @@ interface TabsProps {
|
||||||
* 仅限 tabs-chrome
|
* 仅限 tabs-chrome
|
||||||
*/
|
*/
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否显示图标
|
* @zh_CN 是否显示图标
|
||||||
*/
|
*/
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
|
/**
|
||||||
|
* @zh_CN 标签页风格
|
||||||
|
*/
|
||||||
|
styleType?: TabsStyleType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 选项卡数据
|
* @zh_CN 选项卡数据
|
||||||
|
|
|
@ -41,7 +41,9 @@ if (!preferences.tabbar.persist) {
|
||||||
<TabsView
|
<TabsView
|
||||||
:active="currentActive"
|
:active="currentActive"
|
||||||
:context-menus="createContextMenus"
|
:context-menus="createContextMenus"
|
||||||
|
:dragable="preferences.tabbar.dragable"
|
||||||
:show-icon="showIcon"
|
:show-icon="showIcon"
|
||||||
|
:style-type="preferences.tabbar.styleType"
|
||||||
:tabs="currentTabs"
|
:tabs="currentTabs"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
@sort-tabs="coreTabbarStore.sortTabs"
|
@sort-tabs="coreTabbarStore.sortTabs"
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { $t } from '@vben-core/locales';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben-core/locales';
|
||||||
|
import { SelectOption } from '@vben-core/typings';
|
||||||
|
|
||||||
|
import SelectItem from '../select-item.vue';
|
||||||
import SwitchItem from '../switch-item.vue';
|
import SwitchItem from '../switch-item.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
@ -12,6 +16,28 @@ defineProps<{ disabled?: boolean }>();
|
||||||
const tabbarEnable = defineModel<boolean>('tabbarEnable');
|
const tabbarEnable = defineModel<boolean>('tabbarEnable');
|
||||||
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
|
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
|
||||||
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
||||||
|
const tabbarDragable = defineModel<boolean>('tabbarDragable');
|
||||||
|
const tabbarStyleType = defineModel<string>('tabbarStyleType');
|
||||||
|
|
||||||
|
const styleItems = computed((): SelectOption[] => [
|
||||||
|
{
|
||||||
|
label: $t('preferences.tabbar.styleType.chrome'),
|
||||||
|
value: 'chrome',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('preferences.tabbar.styleType.plain'),
|
||||||
|
value: 'plain',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('preferences.tabbar.styleType.card'),
|
||||||
|
value: 'card',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: $t('preferences.tabbar.styleType.brisk'),
|
||||||
|
value: 'brisk',
|
||||||
|
},
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -24,4 +50,10 @@ const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
||||||
<SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
|
<SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
|
||||||
{{ $t('preferences.tabbar.persist') }}
|
{{ $t('preferences.tabbar.persist') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
|
<SwitchItem v-model="tabbarDragable" :disabled="!tabbarEnable">
|
||||||
|
{{ $t('preferences.tabbar.dragable') }}
|
||||||
|
</SwitchItem>
|
||||||
|
<SelectItem v-model="tabbarStyleType" :items="styleItems">
|
||||||
|
{{ $t('preferences.tabbar.styleType.title') }}
|
||||||
|
</SelectItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -95,6 +95,8 @@ const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
|
||||||
const tabbarEnable = defineModel<boolean>('tabbarEnable');
|
const tabbarEnable = defineModel<boolean>('tabbarEnable');
|
||||||
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
|
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
|
||||||
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
||||||
|
const tabbarDragable = defineModel<boolean>('tabbarDragable');
|
||||||
|
const tabbarStyleType = defineModel<string>('tabbarStyleType');
|
||||||
|
|
||||||
const navigationStyleType = defineModel<NavigationStyleType>(
|
const navigationStyleType = defineModel<NavigationStyleType>(
|
||||||
'navigationStyleType',
|
'navigationStyleType',
|
||||||
|
@ -346,9 +348,11 @@ async function handleReset() {
|
||||||
|
|
||||||
<Block :title="$t('preferences.tabbar.title')">
|
<Block :title="$t('preferences.tabbar.title')">
|
||||||
<Tabbar
|
<Tabbar
|
||||||
|
v-model:tabbar-dragable="tabbarDragable"
|
||||||
v-model:tabbar-enable="tabbarEnable"
|
v-model:tabbar-enable="tabbarEnable"
|
||||||
v-model:tabbar-persist="tabbarPersist"
|
v-model:tabbar-persist="tabbarPersist"
|
||||||
v-model:tabbar-show-icon="tabbarShowIcon"
|
v-model:tabbar-show-icon="tabbarShowIcon"
|
||||||
|
v-model:tabbar-style-type="tabbarStyleType"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Block :title="$t('preferences.widget.title')">
|
<Block :title="$t('preferences.widget.title')">
|
||||||
|
|
Loading…
Reference in New Issue