chore: init project
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export { default as Tabs } from './tabs.vue';
|
||||
export { default as TabsMore } from './tabs-more.vue';
|
||||
export { default as TabsScreen } from './tabs-screen.vue';
|
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
defineOptions({
|
||||
name: 'TabBackground',
|
||||
});
|
||||
|
||||
const { b, e } = useNamespace('tab-background');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="b()">
|
||||
<div :class="e('divider')"></div>
|
||||
<div :class="e('content')"></div>
|
||||
<svg width="10" height="10" :class="e('before')">
|
||||
<path d="M 0 10 A 10 10 0 0 0 10 0 L 10 10 Z" />
|
||||
</svg>
|
||||
<svg width="10" height="10" :class="e('after')">
|
||||
<path d="M 0 0 A 10 10 0 0 0 10 10 L 0 10 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
68
packages/@vben-core/uikit/tabs-ui/src/components/tab.vue
Normal file
68
packages/@vben-core/uikit/tabs-ui/src/components/tab.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
import type { TabItem } from '@vben-core/typings';
|
||||
|
||||
import { IcRoundClose, MdiPin } from '@vben-core/iconify';
|
||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import TabBackground from './tab-background.vue';
|
||||
|
||||
interface Props {
|
||||
affixTab?: boolean;
|
||||
icon?: string;
|
||||
menus: (data: any) => IContextMenuItem[];
|
||||
onlyOne?: boolean;
|
||||
showIcon?: boolean;
|
||||
tab: TabItem;
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Tab',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
icon: '',
|
||||
});
|
||||
const emit = defineEmits<{ close: []; unPushPin: [] }>();
|
||||
|
||||
const { b, e, is } = useNamespace('tab');
|
||||
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
}
|
||||
function handleUnPushPin() {
|
||||
emit('unPushPin');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[b()]">
|
||||
<VbenContextMenu :menus="menus" :handler-data="tab" item-class="pr-4">
|
||||
<div class="h-full">
|
||||
<TabBackground />
|
||||
<div :class="e('content')" :title="title">
|
||||
<VbenIcon v-if="showIcon" :class="e('icon')" :icon="icon" fallback />
|
||||
<span :class="[e('label'), is('hidden-icon', !icon)]">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-show="!affixTab && !onlyOne"
|
||||
:class="e('extra')"
|
||||
@click.stop="handleClose"
|
||||
>
|
||||
<IcRoundClose :class="e('extra-icon')" />
|
||||
</div>
|
||||
<div
|
||||
v-show="affixTab && !onlyOne"
|
||||
:class="[e('extra'), is('pin', true)]"
|
||||
@click.stop="handleUnPushPin"
|
||||
>
|
||||
<MdiPin :class="e('extra-icon')" />
|
||||
</div>
|
||||
</div>
|
||||
</VbenContextMenu>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DropdownMenuProps } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { IcRoundMoreVert } from '@vben-core/iconify';
|
||||
import { VbenDropdownMenu } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineProps<DropdownMenuProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenDropdownMenu :menus="menus">
|
||||
<div
|
||||
class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
>
|
||||
<IcRoundMoreVert />
|
||||
</div>
|
||||
</VbenDropdownMenu>
|
||||
</template>
|
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { IcRoundFitScreen, IcTwotoneFitScreen } from '@vben-core/iconify';
|
||||
|
||||
const screen = defineModel<boolean>('screen');
|
||||
|
||||
function toggleScreen() {
|
||||
screen.value = !screen.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
@click="toggleScreen"
|
||||
>
|
||||
<IcTwotoneFitScreen v-if="screen" />
|
||||
<IcRoundFitScreen v-else />
|
||||
</div>
|
||||
</template>
|
119
packages/@vben-core/uikit/tabs-ui/src/components/tabs.vue
Normal file
119
packages/@vben-core/uikit/tabs-ui/src/components/tabs.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
import { TabItem } from '@vben-core/typings';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import Tab from './tab.vue';
|
||||
|
||||
interface Props {
|
||||
maxWidth?: number;
|
||||
menus?: (data: any) => IContextMenuItem[];
|
||||
minWidth?: number;
|
||||
showIcon?: boolean;
|
||||
tabs?: TabItem[];
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Tabs',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxWidth: 150,
|
||||
menus: () => [],
|
||||
minWidth: 40,
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ close: [string]; unPushPin: [TabItem] }>();
|
||||
|
||||
const gap = 7;
|
||||
|
||||
const active = defineModel<string>('active');
|
||||
const { b, e, is } = useNamespace('tabs-ui');
|
||||
|
||||
const contentRef = ref();
|
||||
const tabWidth = ref<number>(0);
|
||||
|
||||
const layout = () => {
|
||||
const { maxWidth, minWidth, tabs } = props;
|
||||
if (!contentRef.value) {
|
||||
return Math.max(maxWidth, minWidth);
|
||||
}
|
||||
const contentWidth = contentRef.value.clientWidth - gap * 3;
|
||||
let width = contentWidth / tabs.length;
|
||||
width += gap * 2;
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
if (width < minWidth) {
|
||||
width = minWidth;
|
||||
}
|
||||
tabWidth.value = width;
|
||||
};
|
||||
|
||||
const tabsView = computed(() => {
|
||||
return props.tabs.map((tab) => {
|
||||
return {
|
||||
...tab,
|
||||
affixTab: !!tab.meta?.affixTab,
|
||||
icon: tab.meta.icon as string,
|
||||
key: tab.fullPath || tab.path,
|
||||
title: (tab.meta?.title || tab.name) as string,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.tabs,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
layout();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
layout();
|
||||
});
|
||||
|
||||
function handleClose(key: string) {
|
||||
emit('close', key);
|
||||
}
|
||||
function handleUnPushPin(tab: TabItem) {
|
||||
emit('unPushPin', tab);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="b()">
|
||||
<div ref="contentRef" :class="e('content')">
|
||||
<TransitionGroup name="slide-down">
|
||||
<Tab
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
:menus="menus"
|
||||
:tab="tab"
|
||||
:icon="tab.icon"
|
||||
:title="tab.title"
|
||||
:show-icon="showIcon"
|
||||
:affix-tab="tab.affixTab"
|
||||
:only-one="tabsView.length <= 1"
|
||||
:class="[e('tab'), is('active', tab.key === active)]"
|
||||
:style="{
|
||||
width: `${tabWidth}px`,
|
||||
left: `${(tabWidth - gap * 2) * i}px`,
|
||||
}"
|
||||
@click="active = tab.key"
|
||||
@close="() => handleClose(tab.key)"
|
||||
@un-push-pin="() => handleUnPushPin(tab)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '../styles/tabs.scss';
|
||||
</style>
|
Reference in New Issue
Block a user