35 changed files with 662 additions and 518 deletions
@ -0,0 +1,7 @@ |
|||||
|
import { defineBuildConfig } from 'unbuild'; |
||||
|
|
||||
|
export default defineBuildConfig({ |
||||
|
clean: true, |
||||
|
declaration: true, |
||||
|
entries: ['src/index'], |
||||
|
}); |
||||
@ -0,0 +1,45 @@ |
|||||
|
{ |
||||
|
"name": "@vben-core/hooks", |
||||
|
"version": "5.0.0", |
||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin", |
||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
||||
|
"directory": "packages/@vben-core/shared/hooks" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"scripts": { |
||||
|
"build": "pnpm unbuild", |
||||
|
"stub": "pnpm unbuild --stub" |
||||
|
}, |
||||
|
"files": [ |
||||
|
"dist" |
||||
|
], |
||||
|
"sideEffects": false, |
||||
|
"main": "./dist/index.mjs", |
||||
|
"module": "./dist/index.mjs", |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"development": "./src/index.ts", |
||||
|
"default": "./dist/index.mjs" |
||||
|
} |
||||
|
}, |
||||
|
"publishConfig": { |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./dist/index.d.ts", |
||||
|
"default": "./dist/index.mjs" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"sortablejs": "^1.15.2", |
||||
|
"vue": "^3.4.31" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/sortablejs": "^1.15.8" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './use-sortable'; |
||||
@ -0,0 +1,46 @@ |
|||||
|
import type { SortableOptions } from 'sortablejs'; |
||||
|
|
||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { useSortable } from './use-sortable'; |
||||
|
|
||||
|
describe('useSortable', () => { |
||||
|
beforeEach(() => { |
||||
|
vi.mock('sortablejs', () => ({ |
||||
|
default: { |
||||
|
create: vi.fn(), |
||||
|
}, |
||||
|
})); |
||||
|
}); |
||||
|
it('should call Sortable.create with the correct options', async () => { |
||||
|
// Create a mock element
|
||||
|
const mockElement = document.createElement('div') as HTMLDivElement; |
||||
|
|
||||
|
// Define custom options
|
||||
|
const customOptions: SortableOptions = { |
||||
|
group: 'test-group', |
||||
|
sort: false, |
||||
|
}; |
||||
|
|
||||
|
// Use the useSortable function
|
||||
|
const { initializeSortable } = useSortable(mockElement, customOptions); |
||||
|
|
||||
|
// Initialize sortable
|
||||
|
await initializeSortable(); |
||||
|
|
||||
|
// Import sortablejs to access the mocked create function
|
||||
|
const Sortable = await import('sortablejs'); |
||||
|
|
||||
|
// Verify that Sortable.create was called with the correct parameters
|
||||
|
expect(Sortable.default.create).toHaveBeenCalledTimes(1); |
||||
|
expect(Sortable.default.create).toHaveBeenCalledWith( |
||||
|
mockElement, |
||||
|
expect.objectContaining({ |
||||
|
animation: 100, |
||||
|
delay: 400, |
||||
|
delayOnTouchOnly: true, |
||||
|
...customOptions, |
||||
|
}), |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,33 @@ |
|||||
|
import type { SortableOptions } from 'sortablejs'; |
||||
|
|
||||
|
function useSortable<T extends HTMLElement>( |
||||
|
sortableContainer: T, |
||||
|
options: SortableOptions = {}, |
||||
|
) { |
||||
|
const initializeSortable = async () => { |
||||
|
const Sortable = await import( |
||||
|
// @ts-expect-error - This is a dynamic import
|
||||
|
'sortablejs/modular/sortable.complete.esm.js' |
||||
|
); |
||||
|
// const { AutoScroll } = await import(
|
||||
|
// // @ts-expect-error - This is a dynamic import
|
||||
|
// 'sortablejs/modular/sortable.core.esm.js'
|
||||
|
// );
|
||||
|
|
||||
|
// Sortable?.default?.mount?.(AutoScroll);
|
||||
|
|
||||
|
const sortable = Sortable?.default?.create?.(sortableContainer, { |
||||
|
animation: 100, |
||||
|
delay: 400, |
||||
|
delayOnTouchOnly: true, |
||||
|
...options, |
||||
|
}); |
||||
|
return sortable; |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
initializeSortable, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export { useSortable }; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/library.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
import { defineBuildConfig } from 'unbuild'; |
||||
|
|
||||
|
export default defineBuildConfig({ |
||||
|
clean: true, |
||||
|
declaration: true, |
||||
|
entries: [ |
||||
|
{ |
||||
|
builder: 'mkdist', |
||||
|
input: './src', |
||||
|
loaders: ['vue'], |
||||
|
pattern: ['**/*.vue'], |
||||
|
}, |
||||
|
{ |
||||
|
builder: 'mkdist', |
||||
|
format: 'esm', |
||||
|
input: './src', |
||||
|
loaders: ['js'], |
||||
|
pattern: ['**/*.ts'], |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
@ -1,193 +0,0 @@ |
|||||
@import '@vben-core/design/bem'; |
|
||||
|
|
||||
@include b('chrome-tabs') { |
|
||||
--tabs-background: hsl(var(--background)); |
|
||||
--tabs-gap: 7px; |
|
||||
--tabs-divider: hsl(var(--border)); |
|
||||
--tabs-hover: hsl(var(--heavy)); |
|
||||
--tabs-active-background: hsl(var(--primary) / 100%); |
|
||||
--tabs-active: hsl(var(--primary-foreground)); |
|
||||
|
|
||||
background-color: var(--tabs-background); |
|
||||
} |
|
||||
|
|
||||
@include b('chrome-tab') { |
|
||||
color: hsl(var(--muted-foreground)); |
|
||||
|
|
||||
@include is('active') { |
|
||||
z-index: 2; |
|
||||
color: var(--tabs-active); |
|
||||
|
|
||||
.#{$namespace}-chrome-tab__extra:not(.is-pin) { |
|
||||
background-color: var(--tabs-active-background); |
|
||||
opacity: 1; |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__divider { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__content { |
|
||||
background-color: var(--tabs-active-background); |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__before, |
|
||||
.#{$namespace}-chrome-tab-background__after { |
|
||||
fill: var(--tabs-active-background); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@include e('content') { |
|
||||
position: absolute; |
|
||||
right: 0; |
|
||||
left: 0; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
height: 100%; |
|
||||
padding-right: 10px; |
|
||||
margin: 0 calc(var(--tabs-gap) * 2); |
|
||||
overflow: hidden; |
|
||||
border-top-left-radius: 5px; |
|
||||
border-top-right-radius: 5px; |
|
||||
} |
|
||||
|
|
||||
@include e('extra') { |
|
||||
position: absolute; |
|
||||
top: 50%; |
|
||||
right: calc(var(--tabs-gap) * 2); |
|
||||
z-index: 1; |
|
||||
width: 14px; |
|
||||
height: 14px; |
|
||||
border-radius: 50%; |
|
||||
opacity: 0; |
|
||||
transition: 0.15s; |
|
||||
transform: translateY(-50%); |
|
||||
|
|
||||
// &:hover { |
|
||||
// background-color: hsl(var(--accent)); |
|
||||
// } |
|
||||
} |
|
||||
|
|
||||
@include e('extra-icon') { |
|
||||
flex-shrink: 0; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
font-size: 12px; |
|
||||
border-radius: 50%; |
|
||||
transition: all 0.15s ease; |
|
||||
|
|
||||
&:hover { |
|
||||
color: hsl(var(--foreground)); |
|
||||
transform: scale(1.05); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@include e('icon') { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
height: 16px; |
|
||||
margin-left: 3%; |
|
||||
overflow: hidden; |
|
||||
|
|
||||
img { |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@include e('label') { |
|
||||
position: relative; |
|
||||
flex: 1; |
|
||||
margin-right: 8px; |
|
||||
margin-left: 5%; |
|
||||
overflow: hidden; |
|
||||
font-size: 14px; |
|
||||
white-space: nowrap; |
|
||||
mask-image: linear-gradient( |
|
||||
90deg, |
|
||||
#000 0%, |
|
||||
#000 calc(100% - 20px), |
|
||||
transparent |
|
||||
); |
|
||||
|
|
||||
// &.no-close { |
|
||||
// margin-right: 0; |
|
||||
// } |
|
||||
|
|
||||
// &.no-icon { |
|
||||
// margin-left: 0; |
|
||||
// } |
|
||||
} |
|
||||
|
|
||||
@include is('hidden-icon') { |
|
||||
margin-left: 0; |
|
||||
} |
|
||||
|
|
||||
&:hover { |
|
||||
.#{$namespace}-chrome-tab__extra.is-pin { |
|
||||
opacity: 1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&:not(.is-active):hover { |
|
||||
z-index: 1; |
|
||||
|
|
||||
.#{$namespace}-chrome-tab__extra { |
|
||||
opacity: 1; |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__divider { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__content { |
|
||||
background-color: var(--tabs-hover); |
|
||||
} |
|
||||
|
|
||||
.#{$namespace}-chrome-tab-background__before, |
|
||||
.#{$namespace}-chrome-tab-background__after { |
|
||||
fill: var(--tabs-hover); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&:first-of-type { |
|
||||
.#{$namespace}-chrome-tab-background__divider::before { |
|
||||
display: none; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@include b('chrome-tab-background') { |
|
||||
padding: 0 calc(var(--tabs-gap) + 0px); |
|
||||
|
|
||||
@include e('divider') { |
|
||||
width: calc(100% - 14px); |
|
||||
margin: 0 7px; |
|
||||
|
|
||||
&::before { |
|
||||
background-color: var(--tabs-divider); |
|
||||
} |
|
||||
|
|
||||
&::after { |
|
||||
left: calc(100% - 1px); |
|
||||
background-color: var(--tabs-divider); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@include e('content') { |
|
||||
border-top-left-radius: 5px; |
|
||||
border-top-right-radius: 5px; |
|
||||
transition: background 0.15s ease; |
|
||||
} |
|
||||
|
|
||||
@include e('before') { |
|
||||
bottom: -1px; |
|
||||
left: -3px; |
|
||||
transition: 0.15s; |
|
||||
} |
|
||||
|
|
||||
@include e('after') { |
|
||||
right: -3px; |
|
||||
bottom: -1px; |
|
||||
transition: 0.15s; |
|
||||
} |
|
||||
} |
|
||||
@ -1,35 +0,0 @@ |
|||||
<script setup lang="ts"> |
|
||||
import { useNamespace } from '@vben-core/toolkit'; |
|
||||
|
|
||||
defineOptions({ |
|
||||
name: 'ChromeTabBackground', |
|
||||
}); |
|
||||
|
|
||||
const { b, e } = useNamespace('chrome-tab-background'); |
|
||||
</script> |
|
||||
|
|
||||
<template> |
|
||||
<div :class="b()" class="absolute size-full"> |
|
||||
<div |
|
||||
:class="e('divider')" |
|
||||
class="absolute left-0 h-full before:absolute before:right-[100%] before:top-[15%] before:h-[60%] before:w-[1px] before:content-[''] after:absolute after:top-[15%] after:h-[60%] after:w-[1px] after:content-['']" |
|
||||
></div> |
|
||||
<div :class="e('content')" class="h-full"></div> |
|
||||
<svg |
|
||||
:class="e('before')" |
|
||||
class="absolute fill-transparent" |
|
||||
height="10" |
|
||||
width="10" |
|
||||
> |
|
||||
<path d="M 0 10 A 10 10 0 0 0 10 0 L 10 10 Z" /> |
|
||||
</svg> |
|
||||
<svg |
|
||||
:class="e('after')" |
|
||||
class="absolute fill-transparent" |
|
||||
height="10" |
|
||||
width="10" |
|
||||
> |
|
||||
<path d="M 0 0 A 10 10 0 0 0 10 10 L 0 10 Z" /> |
|
||||
</svg> |
|
||||
</div> |
|
||||
</template> |
|
||||
@ -1,76 +0,0 @@ |
|||||
<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: 'ChromeTab', |
|
||||
}); |
|
||||
|
|
||||
withDefaults(defineProps<Props>(), { |
|
||||
icon: '', |
|
||||
}); |
|
||||
const emit = defineEmits<{ close: []; unpinTab: [] }>(); |
|
||||
|
|
||||
const { b, e, is } = useNamespace('chrome-tab'); |
|
||||
|
|
||||
function handleClose() { |
|
||||
emit('close'); |
|
||||
} |
|
||||
function handleUnpinTab() { |
|
||||
emit('unpinTab'); |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<template> |
|
||||
<div |
|
||||
:class="[b()]" |
|
||||
class="absolute flex h-full cursor-pointer select-none items-center" |
|
||||
> |
|
||||
<VbenContextMenu |
|
||||
:handler-data="tab" |
|
||||
:menus="menus" |
|
||||
:modal="false" |
|
||||
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="handleUnpinTab" |
|
||||
> |
|
||||
<MdiPin :class="e('extra-icon')" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</VbenContextMenu> |
|
||||
</div> |
|
||||
</template> |
|
||||
@ -1,114 +0,0 @@ |
|||||
<script setup lang="ts"> |
|
||||
import type { TabItem } from '@vben-core/typings'; |
|
||||
|
|
||||
import type { TabsProps } from '../../types'; |
|
||||
|
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
|
||||
|
|
||||
import { useNamespace } from '@vben-core/toolkit'; |
|
||||
|
|
||||
import Tab from './tab.vue'; |
|
||||
|
|
||||
interface Props extends TabsProps {} |
|
||||
|
|
||||
defineOptions({ |
|
||||
name: 'ChromeTabs', |
|
||||
}); |
|
||||
|
|
||||
const props = withDefaults(defineProps<Props>(), { |
|
||||
maxWidth: 150, |
|
||||
menus: () => [], |
|
||||
minWidth: 40, |
|
||||
tabs: () => [], |
|
||||
}); |
|
||||
|
|
||||
const emit = defineEmits<{ close: [string]; unpinTab: [TabItem] }>(); |
|
||||
|
|
||||
const gap = 7; |
|
||||
|
|
||||
const active = defineModel<string>('active'); |
|
||||
const { b, e, is } = useNamespace('chrome-tabs'); |
|
||||
|
|
||||
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 handleUnpinTab(tab: TabItem) { |
|
||||
emit('unpinTab', tab); |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<template> |
|
||||
<div :class="b()" class="relative size-full pt-1"> |
|
||||
<div ref="contentRef" class="relative h-8 overflow-hidden"> |
|
||||
<TransitionGroup name="slide-down"> |
|
||||
<Tab |
|
||||
v-for="(tab, i) in tabsView" |
|
||||
:key="tab.key" |
|
||||
:affix-tab="tab.affixTab" |
|
||||
:class="[e('tab'), is('active', tab.key === active)]" |
|
||||
:icon="tab.icon" |
|
||||
:menus="menus" |
|
||||
:only-one="tabsView.length <= 1" |
|
||||
:show-icon="showIcon" |
|
||||
:style="{ |
|
||||
width: `${tabWidth}px`, |
|
||||
left: `${(tabWidth - gap * 2) * i}px`, |
|
||||
}" |
|
||||
:tab="tab" |
|
||||
:title="tab.title" |
|
||||
@click="active = tab.key" |
|
||||
@close="() => handleClose(tab.key)" |
|
||||
@unpin-tab="() => handleUnpinTab(tab)" |
|
||||
/> |
|
||||
</TransitionGroup> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
<style lang="scss"> |
|
||||
@import './chrome-tabs.scss'; |
|
||||
</style> |
|
||||
@ -1 +1 @@ |
|||||
export { default as ChromeTabs } from './chrome-tabs/tabs.vue'; |
export { default as TabsChrome } from './tabs-chrome/tabs.vue'; |
||||
|
|||||
@ -0,0 +1,264 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { TabItem } from '@vben-core/typings'; |
||||
|
|
||||
|
import type { TabsProps } from '../../types'; |
||||
|
|
||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
||||
|
|
||||
|
import { MdiPin } from '@vben-core/iconify'; |
||||
|
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
interface Props extends TabsProps {} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'TabsChrome', |
||||
|
// eslint-disable-next-line perfectionist/sort-objects |
||||
|
inheritAttrs: false, |
||||
|
}); |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
contentClass: 'vben-tabs-content', |
||||
|
contextMenus: () => [], |
||||
|
gap: 7, |
||||
|
maxWidth: 150, |
||||
|
minWidth: 40, |
||||
|
tabs: () => [], |
||||
|
}); |
||||
|
|
||||
|
const emit = defineEmits<{ close: [string]; unpin: [TabItem] }>(); |
||||
|
const active = defineModel<string>('active'); |
||||
|
|
||||
|
const contentRef = ref(); |
||||
|
const tabRef = ref(); |
||||
|
const tabWidth = ref<number>(0); |
||||
|
|
||||
|
const style = computed(() => { |
||||
|
const { gap } = props; |
||||
|
return { |
||||
|
'--gap': `${gap}px`, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const layout = () => { |
||||
|
const { gap, 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, |
||||
|
closable: tab.meta?.tabClosable ?? true, |
||||
|
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 handleUnpinTab(tab: TabItem) { |
||||
|
emit('unpin', tab); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :style="style" class="tabs-chrome bg-accent size-full pt-1"> |
||||
|
<!-- footer -> 4px --> |
||||
|
<div |
||||
|
ref="contentRef" |
||||
|
:class="contentClass" |
||||
|
class="relative h-full overflow-hidden" |
||||
|
> |
||||
|
<TransitionGroup name="slide-down"> |
||||
|
<div |
||||
|
v-for="(tab, i) in tabsView" |
||||
|
:key="tab.key" |
||||
|
ref="tabRef" |
||||
|
:class="[ |
||||
|
{ 'is-active': tab.key === active, dragable: !tab.affixTab }, |
||||
|
]" |
||||
|
:data-index="i" |
||||
|
:style="{ |
||||
|
width: `${tabWidth}px`, |
||||
|
left: `${(tabWidth - gap * 2) * i}px`, |
||||
|
}" |
||||
|
class="tabs-chrome__item group absolute flex h-full select-none items-center transition-all" |
||||
|
@click="active = tab.key" |
||||
|
> |
||||
|
<VbenContextMenu |
||||
|
:handler-data="tab" |
||||
|
:menus="contextMenus" |
||||
|
:modal="false" |
||||
|
item-class="pr-6" |
||||
|
> |
||||
|
<div class="size-full"> |
||||
|
<!-- divider --> |
||||
|
<div |
||||
|
v-if="i !== 0" |
||||
|
class="tabs-chrome__divider bg-accent absolute left-[var(--gap)] top-1/2 z-0 h-5 w-[1px] translate-y-[-50%]" |
||||
|
></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 h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150" |
||||
|
></div> |
||||
|
<svg |
||||
|
class="tabs-chrome__background-before absolute bottom-[-1px] 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 absolute bottom-[-1px] right-[-1px] fill-transparent transition-all duration-150" |
||||
|
height="7" |
||||
|
width="7" |
||||
|
> |
||||
|
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" /> |
||||
|
</svg> |
||||
|
</div> |
||||
|
|
||||
|
<!-- extra --> |
||||
|
<div |
||||
|
class="tabs-chrome__extra absolute right-[calc(var(--gap)*2)] top-1/2 z-[3] size-4 translate-y-[-50%] opacity-0 transition-opacity group-hover:opacity-100" |
||||
|
> |
||||
|
<!-- close-icon --> |
||||
|
<svg |
||||
|
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" |
||||
|
height="12" |
||||
|
stroke="#595959" |
||||
|
width="12" |
||||
|
@click.stop="handleClose(tab.key)" |
||||
|
> |
||||
|
<path d="M 4 4 L 12 12 M 12 4 L 4 12" /> |
||||
|
</svg> |
||||
|
<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="tabs-chrome__item-main 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] duration-150 group-hover:pr-3" |
||||
|
> |
||||
|
<VbenIcon |
||||
|
v-if="showIcon" |
||||
|
:icon="tab.icon" |
||||
|
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden" |
||||
|
fallback |
||||
|
/> |
||||
|
|
||||
|
<span |
||||
|
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap" |
||||
|
> |
||||
|
{{ tab.title }} |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</VbenContextMenu> |
||||
|
</div> |
||||
|
</TransitionGroup> |
||||
|
</div> |
||||
|
<!-- footer --> |
||||
|
<div class="bg-background h-1"></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.tabs-chrome { |
||||
|
.dragging { |
||||
|
.tabs-chrome__item-main { |
||||
|
@apply pr-0; |
||||
|
} |
||||
|
|
||||
|
.tabs-chrome__extra { |
||||
|
@apply hidden; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__item { |
||||
|
&:hover { |
||||
|
& + .tabs-chrome__item { |
||||
|
.tabs-chrome__divider { |
||||
|
@apply opacity-0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tabs-chrome__divider { |
||||
|
@apply opacity-0; |
||||
|
} |
||||
|
|
||||
|
.tabs-chrome__background { |
||||
|
&-content { |
||||
|
@apply bg-accent; |
||||
|
} |
||||
|
|
||||
|
&-before, |
||||
|
&-after { |
||||
|
@apply fill-accent; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-active { |
||||
|
@apply z-[2]; |
||||
|
|
||||
|
.tabs-chrome__background { |
||||
|
@apply opacity-100; |
||||
|
|
||||
|
&-content { |
||||
|
@apply bg-background; |
||||
|
} |
||||
|
|
||||
|
&-before, |
||||
|
&-after { |
||||
|
@apply fill-background; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__label { |
||||
|
mask-image: linear-gradient( |
||||
|
90deg, |
||||
|
#000 0%, |
||||
|
#000 calc(100% - 16px), |
||||
|
transparent |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,3 +1,3 @@ |
|||||
|
export * from './components/widgets'; |
||||
export { default as TabsView } from './tabs-view.vue'; |
export { default as TabsView } from './tabs-view.vue'; |
||||
export * from './widgets'; |
|
||||
export type { IContextMenuItem } from '@vben-core/shadcn-ui'; |
export type { IContextMenuItem } from '@vben-core/shadcn-ui'; |
||||
|
|||||
@ -1,3 +0,0 @@ |
|||||
import { defineConfig } from '@vben/vite-config'; |
|
||||
|
|
||||
export default defineConfig(); |
|
||||
@ -1,3 +1,2 @@ |
|||||
export { default as LayoutTabbar } from './tabbar.vue'; |
export { default as LayoutTabbar } from './tabbar.vue'; |
||||
export { default as LayoutTabbarTools } from './tabbar-tools.vue'; |
|
||||
export * from './use-tabs'; |
export * from './use-tabs'; |
||||
|
|||||
@ -1,28 +0,0 @@ |
|||||
<script lang="ts" setup> |
|
||||
import { computed } from 'vue'; |
|
||||
import { useRoute } from 'vue-router'; |
|
||||
|
|
||||
import { preferences } from '@vben-core/preferences'; |
|
||||
import { TabsToolMore, TabsToolScreen } from '@vben-core/tabs-ui'; |
|
||||
|
|
||||
import { updateContentScreen, useTabs } from './use-tabs'; |
|
||||
|
|
||||
const route = useRoute(); |
|
||||
|
|
||||
const { createContextMenus } = useTabs(); |
|
||||
|
|
||||
const menus = computed(() => { |
|
||||
return createContextMenus(route); |
|
||||
}); |
|
||||
</script> |
|
||||
|
|
||||
<template> |
|
||||
<div class="flex-center h-full"> |
|
||||
<TabsToolMore :menus="menus" /> |
|
||||
<TabsToolScreen |
|
||||
:screen="preferences.sidebar.hidden" |
|
||||
@change="updateContentScreen" |
|
||||
@update:screen="updateContentScreen" |
|
||||
/> |
|
||||
</div> |
|
||||
</template> |
|
||||
Loading…
Reference in new issue