init: 项目初始化提交
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
This commit is contained in:
2
packages/@core/ui-kit/tabs-ui/src/components/index.ts
Normal file
2
packages/@core/ui-kit/tabs-ui/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as TabsChrome } from './tabs-chrome/tabs.vue';
|
||||
export { default as Tabs } from './tabs/tabs.vue';
|
||||
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabDefinition } from '@vben-core/typings';
|
||||
|
||||
import type { TabConfig, TabsProps } from '../../types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Pin, X } from '@vben-core/icons';
|
||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props extends TabsProps {}
|
||||
|
||||
defineOptions({
|
||||
name: 'VbenTabsChrome',
|
||||
// eslint-disable-next-line perfectionist/sort-objects
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentClass: 'vben-tabs-content',
|
||||
contextMenus: () => [],
|
||||
gap: 7,
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [string];
|
||||
unpin: [TabDefinition];
|
||||
}>();
|
||||
const active = defineModel<string>('active');
|
||||
|
||||
const contentRef = ref();
|
||||
const tabRef = ref();
|
||||
|
||||
const style = computed(() => {
|
||||
const { gap } = props;
|
||||
return {
|
||||
'--gap': `${gap}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const tabsView = computed(() => {
|
||||
return props.tabs.map((tab) => {
|
||||
const { fullPath, meta, name, path } = tab || {};
|
||||
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
|
||||
return {
|
||||
affixTab: !!affixTab,
|
||||
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
|
||||
fullPath,
|
||||
icon: icon as string,
|
||||
key: fullPath || path,
|
||||
meta,
|
||||
name,
|
||||
path,
|
||||
title: (newTabTitle || title || name) as string,
|
||||
} as TabConfig;
|
||||
});
|
||||
});
|
||||
|
||||
function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
||||
if (
|
||||
e.button === 1 &&
|
||||
tab.closable &&
|
||||
!tab.affixTab &&
|
||||
tabsView.value.length > 1 &&
|
||||
props.middleClickToClose
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
emit('close', tab.key);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="contentRef"
|
||||
:class="contentClass"
|
||||
:style="style"
|
||||
class="tabs-chrome !flex h-full w-max overflow-y-hidden pr-6"
|
||||
>
|
||||
<TransitionGroup name="slide-left">
|
||||
<div
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
ref="tabRef"
|
||||
:class="[
|
||||
{
|
||||
'is-active': tab.key === active,
|
||||
draggable: !tab.affixTab,
|
||||
'affix-tab': tab.affixTab,
|
||||
},
|
||||
]"
|
||||
:data-active-tab="active"
|
||||
:data-index="i"
|
||||
class="tabs-chrome__item draggable translate-all group relative -mr-3 flex h-full select-none items-center"
|
||||
data-tab-item="true"
|
||||
@click="active = tab.key"
|
||||
@mousedown="onMouseDown($event, tab)"
|
||||
>
|
||||
<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-border 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>
|
||||
|
||||
<!-- 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)"
|
||||
/>
|
||||
<Pin
|
||||
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-[1px] 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 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"
|
||||
/>
|
||||
|
||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VbenContextMenu>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tabs-chrome {
|
||||
&__item:not(.dragging) {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
& + .tabs-chrome__item {
|
||||
.tabs-chrome__divider {
|
||||
@apply opacity-0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-chrome__divider {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.tabs-chrome__background {
|
||||
@apply pb-[2px];
|
||||
|
||||
&-content {
|
||||
@apply bg-accent mx-[2px] rounded-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@apply z-[2];
|
||||
|
||||
& + .tabs-chrome__item {
|
||||
.tabs-chrome__divider {
|
||||
@apply opacity-0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
148
packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue
Normal file
148
packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TabDefinition } from '@vben-core/typings';
|
||||
|
||||
import type { TabConfig, TabsProps } from '../../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Pin, X } from '@vben-core/icons';
|
||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
|
||||
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 [&:not(:first-child)]:border-l last:border-r last:border-r border-border`,
|
||||
},
|
||||
card: {
|
||||
content:
|
||||
'h-[calc(100%-6px)] rounded-md ml-2 border border-border transition-all',
|
||||
},
|
||||
plain: {
|
||||
content:
|
||||
'h-full [&:not(:first-child)]:border-l last:border-r border-border',
|
||||
},
|
||||
};
|
||||
|
||||
return typeClasses[props.styleType || 'plain'] || { content: '' };
|
||||
});
|
||||
|
||||
const tabsView = computed(() => {
|
||||
return props.tabs.map((tab) => {
|
||||
const { fullPath, meta, name, path } = tab || {};
|
||||
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
|
||||
return {
|
||||
affixTab: !!affixTab,
|
||||
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
|
||||
fullPath,
|
||||
icon: icon as string,
|
||||
key: fullPath || path,
|
||||
meta,
|
||||
name,
|
||||
path,
|
||||
title: (newTabTitle || title || name) as string,
|
||||
} as TabConfig;
|
||||
});
|
||||
});
|
||||
|
||||
function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
||||
if (
|
||||
e.button === 1 &&
|
||||
tab.closable &&
|
||||
!tab.affixTab &&
|
||||
tabsView.value.length > 1 &&
|
||||
props.middleClickToClose
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
emit('close', tab.key);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="contentClass"
|
||||
class="relative !flex h-full w-max items-center overflow-hidden pr-6"
|
||||
>
|
||||
<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,
|
||||
draggable: !tab.affixTab,
|
||||
'affix-tab': tab.affixTab,
|
||||
},
|
||||
typeWithClass.content,
|
||||
]"
|
||||
:data-index="i"
|
||||
class="tab-item [&:not(.is-active)]:hover:bg-accent translate-all group relative flex cursor-pointer select-none"
|
||||
data-tab-item="true"
|
||||
@click="active = tab.key"
|
||||
@mousedown="onMouseDown($event, tab)"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<!-- 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)"
|
||||
/>
|
||||
<Pin
|
||||
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-[1px] 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
|
||||
/>
|
||||
|
||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VbenContextMenu>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as TabsToolMore } from './tool-more.vue';
|
||||
export { default as TabsToolScreen } from './tool-screen.vue';
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DropdownMenuProps } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { ChevronDown } from '@vben-core/icons';
|
||||
import { VbenDropdownMenu } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineProps<DropdownMenuProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenDropdownMenu :menus="menus" :modal="false">
|
||||
<div
|
||||
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
>
|
||||
<ChevronDown class="size-4" />
|
||||
</div>
|
||||
</VbenDropdownMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fullscreen, Minimize2 } from '@vben-core/icons';
|
||||
|
||||
const screen = defineModel<boolean>('screen');
|
||||
|
||||
function toggleScreen() {
|
||||
screen.value = !screen.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
@click="toggleScreen"
|
||||
>
|
||||
<Minimize2 v-if="screen" class="size-4" />
|
||||
<Fullscreen v-else class="size-4" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user