mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-23 01:30:26 +08:00
refactor(@vben-core/tabs-ui): refactor tabs chrome component
This commit is contained in:
parent
d1a19c525f
commit
fed422e187
6
.gitignore
vendored
6
.gitignore
vendored
@ -2,6 +2,12 @@ node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
*-dist.zip
|
||||
*-dist.tar
|
||||
*-dist.war
|
||||
coverage
|
||||
*.local
|
||||
**/.vitepress/cache
|
||||
|
@ -6,6 +6,7 @@
|
||||
"words": [
|
||||
"clsx",
|
||||
"esno",
|
||||
"demi",
|
||||
"unref",
|
||||
"taze",
|
||||
"acmr",
|
||||
@ -40,7 +41,8 @@
|
||||
"vitepress",
|
||||
"ependencies",
|
||||
"vite",
|
||||
"echarts"
|
||||
"echarts",
|
||||
"sortablejs"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
|
@ -12,6 +12,10 @@ interface TabsState {
|
||||
* @zh_CN 当前打开的标签页列表缓存
|
||||
*/
|
||||
cachedTabs: Set<string>;
|
||||
/**
|
||||
* @zh_CN 拖拽结束的索引
|
||||
*/
|
||||
dragEndIndex: number;
|
||||
/**
|
||||
* @zh_CN 需要排除缓存的标签页
|
||||
*/
|
||||
@ -131,7 +135,6 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 关闭其他标签页
|
||||
* @param tab
|
||||
@ -210,6 +213,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
console.error('Failed to close the tab; only one tab remains open.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 通过key关闭标签页
|
||||
* @param key
|
||||
@ -222,7 +226,6 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
|
||||
await this.closeTab(this.tabs[index], router);
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 固定标签页
|
||||
* @param tab
|
||||
@ -236,6 +239,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
this.addTab(tab);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新标签页
|
||||
*/
|
||||
@ -263,6 +267,17 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
this.addTab(routeToTab(tab));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @zh_CN 设置标签页顺序
|
||||
* @param oldIndex
|
||||
* @param newIndex
|
||||
*/
|
||||
async sortTabs(oldIndex: number, newIndex: number) {
|
||||
const currentTab = this.tabs[oldIndex];
|
||||
this.tabs.splice(oldIndex, 1);
|
||||
this.tabs.splice(newIndex, 0, currentTab);
|
||||
this.dragEndIndex = this.dragEndIndex + 1;
|
||||
},
|
||||
/**
|
||||
* @zh_CN 取消固定标签页
|
||||
* @param tab
|
||||
@ -315,7 +330,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
getTabs(): TabItem[] {
|
||||
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
|
||||
return [...affixTabs, ...normalTabs];
|
||||
return [...affixTabs, ...normalTabs].filter(Boolean);
|
||||
},
|
||||
},
|
||||
persist: [
|
||||
@ -327,6 +342,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
|
||||
],
|
||||
state: (): TabsState => ({
|
||||
cachedTabs: new Set(),
|
||||
dragEndIndex: 0,
|
||||
excludeCachedTabs: new Set(),
|
||||
renderRouteView: true,
|
||||
tabs: [],
|
||||
@ -365,7 +381,7 @@ function cloneTab(route: TabItem): TabItem {
|
||||
* @param tab
|
||||
*/
|
||||
function isAffixTab(tab: TabItem) {
|
||||
return tab.meta?.affixTab ?? false;
|
||||
return tab?.meta?.affixTab ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -178,9 +178,9 @@
|
||||
"persist": "Persist Tabs",
|
||||
"contextMenu": {
|
||||
"reload": "Reload",
|
||||
"close": "Close Tab",
|
||||
"pin": "Pin Tab",
|
||||
"unpin": "Unpin Tab",
|
||||
"close": "Close",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
"closeLeft": "Close Left Tabs",
|
||||
"closeRight": "Close Right Tabs",
|
||||
"closeOther": "Close Other Tabs",
|
||||
|
@ -178,9 +178,9 @@
|
||||
"persist": "持久化标签页",
|
||||
"contextMenu": {
|
||||
"reload": "重新加载",
|
||||
"close": "关闭标签页",
|
||||
"pin": "固定标签页",
|
||||
"unpin": "取消固定标签页",
|
||||
"close": "关闭",
|
||||
"pin": "固定",
|
||||
"unpin": "取消固定",
|
||||
"closeLeft": "关闭左侧标签页",
|
||||
"closeRight": "关闭右侧标签页",
|
||||
"closeOther": "关闭其它标签页",
|
||||
|
7
packages/@core/shared/hooks/build.config.ts
Normal file
7
packages/@core/shared/hooks/build.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
45
packages/@core/shared/hooks/package.json
Normal file
45
packages/@core/shared/hooks/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
packages/@core/shared/hooks/src/index.ts
Normal file
1
packages/@core/shared/hooks/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './use-sortable';
|
46
packages/@core/shared/hooks/src/use-sortable.test.ts
Normal file
46
packages/@core/shared/hooks/src/use-sortable.test.ts
Normal file
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
33
packages/@core/shared/hooks/src/use-sortable.ts
Normal file
33
packages/@core/shared/hooks/src/use-sortable.ts
Normal file
@ -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 };
|
6
packages/@core/shared/hooks/tsconfig.json
Normal file
6
packages/@core/shared/hooks/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -37,7 +37,7 @@ const style = computed((): CSSProperties => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :style="style" class="border-border flex w-full border-b">
|
||||
<section :style="style" class="border-border flex w-full">
|
||||
<slot></slot>
|
||||
<div class="flex items-center">
|
||||
<slot name="toolbar"></slot>
|
||||
|
@ -533,9 +533,6 @@ function handleOpenMenu() {
|
||||
:style="tabbarStyle"
|
||||
>
|
||||
<slot name="tabbar"></slot>
|
||||
<template #toolbar>
|
||||
<slot name="tabbar-tools"></slot>
|
||||
</template>
|
||||
</LayoutTabbar>
|
||||
</div>
|
||||
|
||||
|
21
packages/@core/ui-kit/tabs-ui/build.config.ts
Normal file
21
packages/@core/ui-kit/tabs-ui/build.config.ts
Normal file
@ -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'],
|
||||
},
|
||||
],
|
||||
});
|
@ -11,7 +11,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"build": "pnpm unbuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
@ -38,9 +38,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/design": "workspace:*",
|
||||
"@vben-core/hooks": "workspace:*",
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"vue": "^3.4.31"
|
||||
}
|
||||
|
@ -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>
|
@ -10,7 +10,7 @@ defineProps<DropdownMenuProps>();
|
||||
<template>
|
||||
<VbenDropdownMenu :menus="menus" :modal="false">
|
||||
<div
|
||||
class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-1.5 text-lg font-semibold"
|
||||
class="flex-center hover:bg-muted bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-1.5 text-lg font-semibold"
|
||||
>
|
||||
<IcRoundKeyboardArrowDown class="size-5" />
|
||||
</div>
|
@ -10,7 +10,7 @@ function toggleScreen() {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
class="flex-center hover:bg-muted bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
||||
@click="toggleScreen"
|
||||
>
|
||||
<IcTwotoneFitScreen v-if="screen" />
|
@ -1,3 +1,3 @@
|
||||
export * from './components/widgets';
|
||||
export { default as TabsView } from './tabs-view.vue';
|
||||
export * from './widgets';
|
||||
export type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
|
@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabItem } from '@vben-core/typings';
|
||||
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
import { useSortable } from '@vben-core/hooks';
|
||||
import { useForwardPropsEmits } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { ChromeTabs } from './components';
|
||||
import { TabsChrome } from './components';
|
||||
import { TabsProps } from './types';
|
||||
|
||||
interface Props extends TabsProps {}
|
||||
@ -12,13 +15,86 @@ defineOptions({
|
||||
name: 'TabsView',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentClass: 'vben-tabs-content',
|
||||
dragable: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ close: [string]; unPushPin: [TabItem] }>();
|
||||
const emit = defineEmits<{
|
||||
close: [string];
|
||||
sortTabs: [number, number];
|
||||
unpin: [TabItem];
|
||||
}>();
|
||||
|
||||
const forward = useForwardPropsEmits(props, emit);
|
||||
|
||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
||||
const 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 { 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srcParent = findParentElement(srcElement);
|
||||
|
||||
if (!srcParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!srcParent.classList.contains('dragable')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldIndex !== undefined &&
|
||||
newIndex !== undefined &&
|
||||
!Number.isNaN(oldIndex) &&
|
||||
!Number.isNaN(newIndex) &&
|
||||
oldIndex !== newIndex
|
||||
) {
|
||||
emit('sortTabs', oldIndex, newIndex);
|
||||
}
|
||||
el.classList.remove('dragging');
|
||||
el.style.cursor = 'default';
|
||||
},
|
||||
onMove(evt) {
|
||||
const parent = findParentElement(evt.related);
|
||||
return parent?.classList.contains('dragable');
|
||||
},
|
||||
onStart: () => {
|
||||
el.style.cursor = 'grabbing';
|
||||
el.classList.add('dragging');
|
||||
},
|
||||
});
|
||||
|
||||
await initializeSortable();
|
||||
}
|
||||
|
||||
onMounted(initTabsSortable);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChromeTabs v-bind="forward" />
|
||||
<TabsChrome v-bind="forward" />
|
||||
</template>
|
||||
|
@ -2,10 +2,45 @@ import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
import type { TabItem } from '@vben-core/typings';
|
||||
|
||||
interface TabsProps {
|
||||
/**
|
||||
* @zh_CN content class
|
||||
* @default tabs-chrome
|
||||
*/
|
||||
contentClass?: string;
|
||||
/**
|
||||
* @zh_CN 右键菜单
|
||||
*/
|
||||
contextMenus?: (data: any) => IContextMenuItem[];
|
||||
/**
|
||||
* @zh_CN 是否可以拖拽
|
||||
*/
|
||||
dragable?: boolean;
|
||||
/**
|
||||
* @zh_CN 间隙
|
||||
* @default 7
|
||||
* 仅限 tabs-chrome
|
||||
*/
|
||||
gap?: number;
|
||||
|
||||
/**
|
||||
* @zh_CN tab 最大宽度
|
||||
* 仅限 tabs-chrome
|
||||
*/
|
||||
maxWidth?: number;
|
||||
menus?: (data: any) => IContextMenuItem[];
|
||||
|
||||
/**
|
||||
* @zh_CN tab最小宽度
|
||||
* 仅限 tabs-chrome
|
||||
*/
|
||||
minWidth?: number;
|
||||
/**
|
||||
* @zh_CN 是否显示图标
|
||||
*/
|
||||
showIcon?: boolean;
|
||||
|
||||
/**
|
||||
* @zh_CN 选项卡数据
|
||||
*/
|
||||
tabs?: TabItem[];
|
||||
}
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig();
|
@ -24,7 +24,7 @@ import {
|
||||
useExtraMenu,
|
||||
useMixedMenu,
|
||||
} from './menu';
|
||||
import { LayoutTabbar, LayoutTabbarTools } from './tabbar';
|
||||
import { LayoutTabbar } from './tabbar';
|
||||
|
||||
defineOptions({ name: 'BasicLayout' });
|
||||
|
||||
@ -260,9 +260,6 @@ function clearPreferencesAndLogout() {
|
||||
:show-icon="preferences.tabbar.showIcon"
|
||||
/>
|
||||
</template>
|
||||
<template #tabbar-tools>
|
||||
<LayoutTabbarTools v-if="preferences.tabbar.enable" />
|
||||
</template>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<template #content>
|
||||
|
@ -1,3 +1,2 @@
|
||||
export { default as LayoutTabbar } from './tabbar.vue';
|
||||
export { default as LayoutTabbarTools } from './tabbar-tools.vue';
|
||||
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>
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useCoreTabbarStore } from '@vben-core/stores';
|
||||
import { TabsView } from '@vben-core/tabs-ui';
|
||||
import { TabsToolMore, TabsToolScreen, TabsView } from '@vben-core/tabs-ui';
|
||||
|
||||
import { useTabs } from './use-tabs';
|
||||
import { updateContentScreen, useTabs } from './use-tabs';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutTabbar',
|
||||
@ -13,10 +14,10 @@ defineOptions({
|
||||
|
||||
defineProps<{ showIcon?: boolean }>();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const coreTabbarStore = useCoreTabbarStore();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const {
|
||||
createContextMenus,
|
||||
currentActive,
|
||||
@ -26,6 +27,10 @@ const {
|
||||
handleUnpinTab,
|
||||
} = useTabs();
|
||||
|
||||
const menus = computed(() => {
|
||||
return createContextMenus(route);
|
||||
});
|
||||
|
||||
// 刷新后如果不保持tab状态,关闭其他tab
|
||||
if (!preferences.tabbar.persist) {
|
||||
coreTabbarStore.closeOtherTabs(route);
|
||||
@ -35,11 +40,20 @@ if (!preferences.tabbar.persist) {
|
||||
<template>
|
||||
<TabsView
|
||||
:active="currentActive"
|
||||
:menus="createContextMenus"
|
||||
:context-menus="createContextMenus"
|
||||
:show-icon="showIcon"
|
||||
:tabs="currentTabs"
|
||||
@close="handleClose"
|
||||
@unpin-tab="handleUnpinTab"
|
||||
@sort-tabs="coreTabbarStore.sortTabs"
|
||||
@unpin="handleUnpinTab"
|
||||
@update:active="handleClick"
|
||||
/>
|
||||
<div class="flex-center h-full">
|
||||
<TabsToolMore :menus="menus" />
|
||||
<TabsToolScreen
|
||||
:screen="preferences.sidebar.hidden"
|
||||
@change="updateContentScreen"
|
||||
@update:screen="updateContentScreen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -83,8 +83,8 @@ function useTabs() {
|
||||
return {
|
||||
...tab,
|
||||
meta: {
|
||||
...tab.meta,
|
||||
title: $t(tab.meta.title as string),
|
||||
...tab?.meta,
|
||||
title: $t(tab?.meta?.title as string),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -126,6 +126,27 @@ function useTabs() {
|
||||
disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;
|
||||
|
||||
const menus: IContextMenuItem[] = [
|
||||
{
|
||||
disabled: !!affixTab || disabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeTab(tab, router);
|
||||
},
|
||||
icon: IcRoundClose,
|
||||
key: 'close',
|
||||
text: $t('preferences.tabbar.contextMenu.close'),
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
await (affixTab
|
||||
? coreTabbarStore.unpinTab(tab)
|
||||
: coreTabbarStore.pinTab(tab));
|
||||
},
|
||||
icon: affixTab ? MdiPinOff : MdiPin,
|
||||
key: 'affix',
|
||||
text: affixTab
|
||||
? $t('preferences.tabbar.contextMenu.unpin')
|
||||
: $t('preferences.tabbar.contextMenu.pin'),
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
if (!contentIsMaximize.value) {
|
||||
@ -148,27 +169,7 @@ function useTabs() {
|
||||
key: 'reload',
|
||||
text: $t('preferences.tabbar.contextMenu.reload'),
|
||||
},
|
||||
{
|
||||
disabled: !!affixTab || disabled,
|
||||
handler: async () => {
|
||||
await coreTabbarStore.closeTab(tab, router);
|
||||
},
|
||||
icon: IcRoundClose,
|
||||
key: 'close',
|
||||
text: $t('preferences.tabbar.contextMenu.close'),
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
await (affixTab
|
||||
? coreTabbarStore.unpinTab(tab)
|
||||
: coreTabbarStore.pinTab(tab));
|
||||
},
|
||||
icon: affixTab ? MdiPinOff : MdiPin,
|
||||
key: 'affix',
|
||||
text: affixTab
|
||||
? $t('preferences.tabbar.contextMenu.unpin')
|
||||
: $t('preferences.tabbar.contextMenu.pin'),
|
||||
},
|
||||
|
||||
{
|
||||
handler: async () => {
|
||||
const { hash, origin } = location;
|
||||
|
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@ -653,6 +653,19 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
|
||||
packages/@core/shared/hooks:
|
||||
dependencies:
|
||||
sortablejs:
|
||||
specifier: ^1.15.2
|
||||
version: 1.15.2
|
||||
vue:
|
||||
specifier: ^3.4.31
|
||||
version: 3.4.31(typescript@5.5.3)
|
||||
devDependencies:
|
||||
'@types/sortablejs':
|
||||
specifier: ^1.15.8
|
||||
version: 1.15.8
|
||||
|
||||
packages/@core/shared/iconify:
|
||||
dependencies:
|
||||
'@iconify/vue':
|
||||
@ -785,15 +798,15 @@ importers:
|
||||
'@vben-core/design':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/design
|
||||
'@vben-core/hooks':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/hooks
|
||||
'@vben-core/iconify':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/iconify
|
||||
'@vben-core/shadcn-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../shadcn-ui
|
||||
'@vben-core/toolkit':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/toolkit
|
||||
'@vben-core/typings':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/typings
|
||||
@ -862,7 +875,7 @@ importers:
|
||||
version: link:../../types
|
||||
'@vueuse/integrations':
|
||||
specifier: ^10.11.0
|
||||
version: 10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(vue@3.4.31(typescript@5.5.3))
|
||||
version: 10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(sortablejs@1.15.2)(vue@3.4.31(typescript@5.5.3))
|
||||
qrcode:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
@ -976,7 +989,7 @@ importers:
|
||||
devDependencies:
|
||||
vitepress:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(@algolia/client-search@4.24.0)(@types/node@20.14.10)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.39)(qrcode@1.5.3)(sass@1.77.8)(search-insights@2.15.0)(terser@5.31.2)(typescript@5.5.3)
|
||||
version: 1.3.0(@algolia/client-search@4.24.0)(@types/node@20.14.10)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.39)(qrcode@1.5.3)(sass@1.77.8)(search-insights@2.15.0)(sortablejs@1.15.2)(terser@5.31.2)(typescript@5.5.3)
|
||||
vue:
|
||||
specifier: ^3.4.31
|
||||
version: 3.4.31(typescript@5.5.3)
|
||||
@ -3008,7 +3021,6 @@ packages:
|
||||
|
||||
'@ls-lint/ls-lint@2.2.3':
|
||||
resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
|
||||
cpu: [x64, arm64, s390x]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@ -3585,6 +3597,9 @@ packages:
|
||||
'@types/serve-static@1.15.7':
|
||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
||||
|
||||
'@types/sortablejs@1.15.8':
|
||||
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
@ -8188,6 +8203,9 @@ packages:
|
||||
resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
sortablejs@1.15.2:
|
||||
resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==}
|
||||
|
||||
source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -12315,6 +12333,8 @@ snapshots:
|
||||
'@types/node': 20.14.10
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/sortablejs@1.15.8': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
@ -12640,7 +12660,7 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/integrations@10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(vue@3.4.31(typescript@5.5.3))':
|
||||
'@vueuse/integrations@10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(sortablejs@1.15.2)(vue@3.4.31(typescript@5.5.3))':
|
||||
dependencies:
|
||||
'@vueuse/core': 10.11.0(vue@3.4.31(typescript@5.5.3))
|
||||
'@vueuse/shared': 10.11.0(vue@3.4.31(typescript@5.5.3))
|
||||
@ -12651,6 +12671,7 @@ snapshots:
|
||||
focus-trap: 7.5.4
|
||||
nprogress: 0.2.0
|
||||
qrcode: 1.5.3
|
||||
sortablejs: 1.15.2
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
@ -17378,6 +17399,8 @@ snapshots:
|
||||
ip-address: 9.0.5
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
sortablejs@1.15.2: {}
|
||||
|
||||
source-map-js@1.2.0: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
@ -18323,7 +18346,7 @@ snapshots:
|
||||
sass: 1.77.8
|
||||
terser: 5.31.2
|
||||
|
||||
vitepress@1.3.0(@algolia/client-search@4.24.0)(@types/node@20.14.10)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.39)(qrcode@1.5.3)(sass@1.77.8)(search-insights@2.15.0)(terser@5.31.2)(typescript@5.5.3):
|
||||
vitepress@1.3.0(@algolia/client-search@4.24.0)(@types/node@20.14.10)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.39)(qrcode@1.5.3)(sass@1.77.8)(search-insights@2.15.0)(sortablejs@1.15.2)(terser@5.31.2)(typescript@5.5.3):
|
||||
dependencies:
|
||||
'@docsearch/css': 3.6.0
|
||||
'@docsearch/js': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0)
|
||||
@ -18334,7 +18357,7 @@ snapshots:
|
||||
'@vue/devtools-api': 7.3.5
|
||||
'@vue/shared': 3.4.31
|
||||
'@vueuse/core': 10.11.0(vue@3.4.31(typescript@5.5.3))
|
||||
'@vueuse/integrations': 10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(vue@3.4.31(typescript@5.5.3))
|
||||
'@vueuse/integrations': 10.11.0(async-validator@4.2.5)(axios@1.7.2)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.3)(sortablejs@1.15.2)(vue@3.4.31(typescript@5.5.3))
|
||||
focus-trap: 7.5.4
|
||||
mark.js: 8.11.1
|
||||
minisearch: 6.3.0
|
||||
|
@ -72,6 +72,10 @@
|
||||
"name": "@vben-core/design",
|
||||
"path": "packages/@core/shared/design",
|
||||
},
|
||||
{
|
||||
"name": "@vben-core/hooks",
|
||||
"path": "packages/@core/shared/hooks",
|
||||
},
|
||||
{
|
||||
"name": "@vben-core/iconify",
|
||||
"path": "packages/@core/shared/iconify",
|
||||
|
Loading…
Reference in New Issue
Block a user