feat: configurable persistent tabs

pull/48/MERGE
vince 2024-07-10 00:50:41 +08:00
parent 41d0495630
commit db76325d68
23 changed files with 177 additions and 176 deletions

View File

@ -33,8 +33,8 @@ function createRequestClient() {
// 这里不能用 useAccessStore因为 useAccessStore 会导致循环引用 // 这里不能用 useAccessStore因为 useAccessStore 会导致循环引用
const accessStore = useCoreAccessStore(); const accessStore = useCoreAccessStore();
return { return {
refreshToken: `Bearer ${accessStore.getRefreshToken}`, refreshToken: `Bearer ${accessStore.refreshToken}`,
token: `Bearer ${accessStore.getAccessToken}`, token: `Bearer ${accessStore.accessToken}`,
}; };
}, },
// 默认 // 默认

View File

@ -17,10 +17,10 @@ export const useAccessStore = defineStore('access', () => {
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const accessToken = computed(() => coreStoreAccess.getAccessToken); const accessToken = computed(() => coreStoreAccess.accessToken);
const userRoles = computed(() => coreStoreAccess.getUserRoles); const userRoles = computed(() => coreStoreAccess.userRoles);
const userInfo = computed(() => coreStoreAccess.getUserInfo); const userInfo = computed(() => coreStoreAccess.userInfo);
const accessRoutes = computed(() => coreStoreAccess.getAccessRoutes); const accessRoutes = computed(() => coreStoreAccess.accessRoutes);
function setAccessMenus(menus: MenuRecordRaw[]) { function setAccessMenus(menus: MenuRecordRaw[]) {
coreStoreAccess.setAccessMenus(menus); coreStoreAccess.setAccessMenus(menus);

View File

@ -71,6 +71,7 @@ const defaultPreferences: Preferences = {
tabbar: { tabbar: {
enable: true, enable: true,
keepAlive: true, keepAlive: true,
persist: true,
showIcon: true, showIcon: true,
}, },
theme: { theme: {

View File

@ -143,6 +143,8 @@ interface TabbarPreferences {
enable: boolean; enable: boolean;
/** 开启标签页缓存功能 */ /** 开启标签页缓存功能 */
keepAlive: boolean; keepAlive: boolean;
/** 是否持久化标签 */
persist: boolean;
/** 是否开启多标签页图标 */ /** 是否开启多标签页图标 */
showIcon: boolean; showIcon: boolean;
} }

View File

@ -33,7 +33,7 @@ describe('useCoreAccessStore', () => {
const store = useCoreAccessStore(); const store = useCoreAccessStore();
const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] }; const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] };
store.setUserInfo(userInfo); store.setUserInfo(userInfo);
expect(store.getUserInfo).toEqual(userInfo); expect(store.userInfo).toEqual(userInfo);
}); });
it('updates accessToken state correctly', () => { it('updates accessToken state correctly', () => {
@ -60,13 +60,13 @@ describe('useCoreAccessStore', () => {
it('returns the correct accessToken', () => { it('returns the correct accessToken', () => {
const store = useCoreAccessStore(); const store = useCoreAccessStore();
store.setAccessToken('xyz789'); store.setAccessToken('xyz789');
expect(store.getAccessToken).toBe('xyz789'); expect(store.accessToken).toBe('xyz789');
}); });
// 测试在没有用户角色时返回空数组 // 测试在没有用户角色时返回空数组
it('returns an empty array for userRoles if not set', () => { it('returns an empty array for userRoles if not set', () => {
const store = useCoreAccessStore(); const store = useCoreAccessStore();
expect(store.getUserRoles).toEqual([]); expect(store.userRoles).toEqual([]);
}); });
// 测试设置空的访问菜单列表 // 测试设置空的访问菜单列表

View File

@ -90,32 +90,6 @@ const useCoreAccessStore = defineStore('core-access', {
this.userRoles = roles; this.userRoles = roles;
}, },
}, },
getters: {
getAccessCodes(): string[] {
return this.accessCodes;
},
getAccessMenus(): MenuRecordRaw[] {
return this.accessMenus;
},
getAccessRoutes(): RouteRecordRaw[] {
return this.accessRoutes;
},
getAccessToken(): AccessToken {
return this.accessToken;
},
getRefreshToken(): AccessToken {
return this.refreshToken;
},
getUserInfo(): BasicUserInfo | null {
return this.userInfo;
},
getUserRoles(): string[] {
return this.userRoles;
},
string(): string[] {
return this.accessCodes;
},
},
persist: { persist: {
// 持久化 // 持久化
paths: ['accessToken', 'refreshToken', 'accessCodes'], paths: ['accessToken', 'refreshToken', 'accessCodes'],

View File

@ -99,9 +99,9 @@ describe('useCoreAccessStore', () => {
it('returns all cache tabs', () => { it('returns all cache tabs', () => {
const store = useCoreTabbarStore(); const store = useCoreTabbarStore();
store.cacheTabs.add('Home'); store.cachedTabs.add('Home');
store.cacheTabs.add('About'); store.cachedTabs.add('About');
expect(store.getCacheTabs).toEqual(['Home', 'About']); expect(store.cachedTabs).toEqual(['Home', 'About']);
}); });
it('returns all tabs, including affix tabs', () => { it('returns all tabs, including affix tabs', () => {
@ -290,7 +290,7 @@ describe('useCoreAccessStore', () => {
await store.refresh(router); await store.refresh(router);
expect(store.excludeCacheTabs.has('Dashboard')).toBe(false); expect(store.excludeCachedTabs.has('Dashboard')).toBe(false);
expect(store.renderRouteView).toBe(true); expect(store.renderRouteView).toBe(true);
}); });
}); });

View File

@ -7,60 +7,15 @@ import { startProgress, stopProgress } from '@vben-core/toolkit';
import { acceptHMRUpdate, defineStore } from 'pinia'; import { acceptHMRUpdate, defineStore } from 'pinia';
/**
* @zh_CN ,
* @param route
*/
function cloneTab(route: TabItem): TabItem {
if (!route) {
return route;
}
const { matched, ...opt } = route;
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path,
}))
: undefined) as RouteRecordNormalized[],
};
}
/**
* @zh_CN
* @param tab
*/
function isAffixTab(tab: TabItem) {
return tab.meta?.affixTab ?? false;
}
/**
* @zh_CN
* @param tab
*/
function isTabShow(tab: TabItem) {
return !tab.meta.hideInTab;
}
function routeToTab(route: RouteRecordNormalized) {
return {
meta: route.meta,
name: route.name,
path: route.path,
} as unknown as TabItem;
}
interface TabsState { interface TabsState {
/** /**
* @zh_CN * @zh_CN
*/ */
cacheTabs: Set<string>; cachedTabs: Set<string>;
/** /**
* @zh_CN * @zh_CN
*/ */
excludeCacheTabs: Set<string>; excludeCachedTabs: Set<string>;
/** /**
* @zh_CN * @zh_CN
*/ */
@ -81,7 +36,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
*/ */
async _bulkCloseByPaths(paths: string[]) { async _bulkCloseByPaths(paths: string[]) {
this.tabs = this.tabs.filter((item) => { this.tabs = this.tabs.filter((item) => {
return !paths.includes(this.getTabPath(item)); return !paths.includes(getTabPath(item));
}); });
this.updateCacheTab(); this.updateCacheTab();
@ -128,12 +83,12 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
*/ */
addTab(routeTab: TabItem) { addTab(routeTab: TabItem) {
const tab = cloneTab(routeTab); const tab = cloneTab(routeTab);
if (!isTabShow(tab)) { if (!isTabShown(tab)) {
return; return;
} }
const tabIndex = this.tabs.findIndex((tab) => { const tabIndex = this.tabs.findIndex((tab) => {
return this.getTabPath(tab) === this.getTabPath(routeTab); return getTabPath(tab) === getTabPath(routeTab);
}); });
if (tabIndex === -1) { if (tabIndex === -1) {
@ -159,19 +114,22 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
*/ */
async closeLeftTabs(tab: TabItem) { async closeLeftTabs(tab: TabItem) {
const index = this.tabs.findIndex( const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab), (item) => getTabPath(item) === getTabPath(tab),
); );
if (index > 0) { if (index < 1) {
const leftTabs = this.tabs.slice(0, index); return;
const paths: string[] = [];
for (const item of leftTabs) {
if (!isAffixTab(tab)) {
paths.push(this.getTabPath(item));
}
}
await this._bulkCloseByPaths(paths);
} }
const leftTabs = this.tabs.slice(0, index);
const paths: string[] = [];
for (const item of leftTabs) {
if (!isAffixTab(item)) {
paths.push(getTabPath(item));
}
}
await this._bulkCloseByPaths(paths);
}, },
/** /**
@ -179,20 +137,18 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
* @param tab * @param tab
*/ */
async closeOtherTabs(tab: TabItem) { async closeOtherTabs(tab: TabItem) {
const closePaths = this.tabs.map((item) => this.getTabPath(item)); const closePaths = this.tabs.map((item) => getTabPath(item));
const paths: string[] = []; const paths: string[] = [];
for (const path of closePaths) { for (const path of closePaths) {
if (path !== tab.fullPath) { if (path !== tab.fullPath) {
const closeTab = this.tabs.find( const closeTab = this.tabs.find((item) => getTabPath(item) === path);
(item) => this.getTabPath(item) === path,
);
if (!closeTab) { if (!closeTab) {
continue; continue;
} }
if (!isAffixTab(tab)) { if (!isAffixTab(closeTab)) {
paths.push(this.getTabPath(closeTab)); paths.push(getTabPath(closeTab));
} }
} }
} }
@ -205,7 +161,7 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
*/ */
async closeRightTabs(tab: TabItem) { async closeRightTabs(tab: TabItem) {
const index = this.tabs.findIndex( const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab), (item) => getTabPath(item) === getTabPath(tab),
); );
if (index >= 0 && index < this.tabs.length - 1) { if (index >= 0 && index < this.tabs.length - 1) {
@ -213,8 +169,8 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
const paths: string[] = []; const paths: string[] = [];
for (const item of rightTabs) { for (const item of rightTabs) {
if (!isAffixTab(tab)) { if (!isAffixTab(item)) {
paths.push(this.getTabPath(item)); paths.push(getTabPath(item));
} }
} }
await this._bulkCloseByPaths(paths); await this._bulkCloseByPaths(paths);
@ -230,13 +186,13 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
const { currentRoute } = router; const { currentRoute } = router;
// 关闭不是激活选项卡 // 关闭不是激活选项卡
if (this.getTabPath(currentRoute.value) !== this.getTabPath(tab)) { if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
this._close(tab); this._close(tab);
this.updateCacheTab(); this.updateCacheTab();
return; return;
} }
const index = this.getTabs.findIndex( const index = this.getTabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(currentRoute.value), (item) => getTabPath(item) === getTabPath(currentRoute.value),
); );
const before = this.getTabs[index - 1]; const before = this.getTabs[index - 1];
@ -259,25 +215,21 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
* @param key * @param key
*/ */
async closeTabByKey(key: string, router: Router) { async closeTabByKey(key: string, router: Router) {
const index = this.tabs.findIndex( const index = this.tabs.findIndex((item) => getTabPath(item) === key);
(item) => this.getTabPath(item) === key,
);
if (index === -1) { if (index === -1) {
return; return;
} }
await this.closeTab(this.tabs[index], router); await this.closeTab(this.tabs[index], router);
}, },
getTabPath(tab: RouteRecordNormalized | TabItem) {
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
},
/** /**
* @zh_CN * @zh_CN
* @param tab * @param tab
*/ */
async pushPinTab(tab: TabItem) { async pinTab(tab: TabItem) {
const index = this.tabs.findIndex( const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab), (item) => getTabPath(item) === getTabPath(tab),
); );
if (index !== -1) { if (index !== -1) {
tab.meta.affixTab = true; tab.meta.affixTab = true;
@ -291,13 +243,13 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
const { currentRoute } = router; const { currentRoute } = router;
const { name } = currentRoute.value; const { name } = currentRoute.value;
this.excludeCacheTabs.add(name as string); this.excludeCachedTabs.add(name as string);
this.renderRouteView = false; this.renderRouteView = false;
startProgress(); startProgress();
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
this.excludeCacheTabs.delete(name as string); this.excludeCachedTabs.delete(name as string);
this.renderRouteView = true; this.renderRouteView = true;
stopProgress(); stopProgress();
}, },
@ -315,9 +267,9 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
* @zh_CN * @zh_CN
* @param tab * @param tab
*/ */
async unPushPinTab(tab: TabItem) { async unpinTab(tab: TabItem) {
const index = this.tabs.findIndex( const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab), (item) => getTabPath(item) === getTabPath(tab),
); );
if (index !== -1) { if (index !== -1) {
@ -347,33 +299,32 @@ const useCoreTabbarStore = defineStore('core-tabbar', {
const name = tab.name as string; const name = tab.name as string;
cacheMap.add(name); cacheMap.add(name);
} }
this.cacheTabs = cacheMap; this.cachedTabs = cacheMap;
}, },
}, },
getters: { getters: {
affixTabs(): TabItem[] { affixTabs(): TabItem[] {
return this.tabs.filter((tab) => isAffixTab(tab)); return this.tabs.filter((tab) => isAffixTab(tab));
}, },
getCacheTabs(): string[] { getCachedTabs(): string[] {
return [...this.cacheTabs]; return [...this.cachedTabs];
}, },
getExcludeTabs(): string[] { getExcludeCachedTabs(): string[] {
return [...this.excludeCacheTabs]; return [...this.excludeCachedTabs];
}, },
getTabs(): TabItem[] { getTabs(): TabItem[] {
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab)); const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab)); const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
return [...affixTabs, ...normalTabs]; return [...affixTabs, ...normalTabs];
}, },
}, },
persist: { persist: {
// 持久化 // 持久化
paths: [], paths: ['tabs'],
}, },
state: (): TabsState => ({ state: (): TabsState => ({
cacheTabs: new Set(), cachedTabs: new Set(),
excludeCacheTabs: new Set(), excludeCachedTabs: new Set(),
renderRouteView: true, renderRouteView: true,
tabs: [], tabs: [],
}), }),
@ -385,4 +336,57 @@ if (hot) {
hot.accept(acceptHMRUpdate(useCoreTabbarStore, hot)); hot.accept(acceptHMRUpdate(useCoreTabbarStore, hot));
} }
/**
* @zh_CN ,
* @param route
*/
function cloneTab(route: TabItem): TabItem {
if (!route) {
return route;
}
const { matched, ...opt } = route;
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path,
}))
: undefined) as RouteRecordNormalized[],
};
}
/**
* @zh_CN
* @param tab
*/
function isAffixTab(tab: TabItem) {
return tab.meta?.affixTab ?? false;
}
/**
* @zh_CN
* @param tab
*/
function isTabShown(tab: TabItem) {
return !tab.meta.hideInTab;
}
/**
* @zh_CN
* @param tab
*/
function getTabPath(tab: RouteRecordNormalized | TabItem) {
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
}
function routeToTab(route: RouteRecordNormalized) {
return {
meta: route.meta,
name: route.name,
path: route.path,
} as TabItem;
}
export { useCoreTabbarStore }; export { useCoreTabbarStore };

View File

@ -2,8 +2,7 @@ import { createPinia } from 'pinia';
interface InitStoreOptions { interface InitStoreOptions {
/** /**
* @zh_CN , @vben-core/stores appapp * @zh_CN , @vben-core/stores appapp,
*
*/ */
namespace: string; namespace: string;
} }

View File

@ -159,6 +159,7 @@
"title": "Tabbar", "title": "Tabbar",
"enable": "Enable Tab Bar", "enable": "Enable Tab Bar",
"icon": "Display Tabbar Icon", "icon": "Display Tabbar Icon",
"persist": "Persistent tabs",
"context-menu": { "context-menu": {
"reload": "Reload", "reload": "Reload",
"close": "Close", "close": "Close",

View File

@ -158,6 +158,7 @@
"title": "标签栏", "title": "标签栏",
"enable": "启用标签栏", "enable": "启用标签栏",
"icon": "显示标签栏图标", "icon": "显示标签栏图标",
"persist": "持久化标签页",
"context-menu": { "context-menu": {
"reload": "重新加载", "reload": "重新加载",
"close": "关闭标签页", "close": "关闭标签页",

View File

@ -25,15 +25,15 @@ defineOptions({
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
icon: '', icon: '',
}); });
const emit = defineEmits<{ close: []; unPushPin: [] }>(); const emit = defineEmits<{ close: []; unpinTab: [] }>();
const { b, e, is } = useNamespace('chrome-tab'); const { b, e, is } = useNamespace('chrome-tab');
function handleClose() { function handleClose() {
emit('close'); emit('close');
} }
function handleUnPushPin() { function handleUnpinTab() {
emit('unPushPin'); emit('unpinTab');
} }
</script> </script>
@ -66,7 +66,7 @@ function handleUnPushPin() {
<div <div
v-show="affixTab && !onlyOne" v-show="affixTab && !onlyOne"
:class="[e('extra'), is('pin', true)]" :class="[e('extra'), is('pin', true)]"
@click.stop="handleUnPushPin" @click.stop="handleUnpinTab"
> >
<MdiPin :class="e('extra-icon')" /> <MdiPin :class="e('extra-icon')" />
</div> </div>

View File

@ -22,7 +22,7 @@ const props = withDefaults(defineProps<Props>(), {
tabs: () => [], tabs: () => [],
}); });
const emit = defineEmits<{ close: [string]; unPushPin: [TabItem] }>(); const emit = defineEmits<{ close: [string]; unpinTab: [TabItem] }>();
const gap = 7; const gap = 7;
@ -77,8 +77,8 @@ onMounted(() => {
function handleClose(key: string) { function handleClose(key: string) {
emit('close', key); emit('close', key);
} }
function handleUnPushPin(tab: TabItem) { function handleUnpinTab(tab: TabItem) {
emit('unPushPin', tab); emit('unpinTab', tab);
} }
</script> </script>
@ -103,7 +103,7 @@ function handleUnPushPin(tab: TabItem) {
:title="tab.title" :title="tab.title"
@click="active = tab.key" @click="active = tab.key"
@close="() => handleClose(tab.key)" @close="() => handleClose(tab.key)"
@un-push-pin="() => handleUnPushPin(tab)" @unpin-tab="() => handleUnpinTab(tab)"
/> />
</TransitionGroup> </TransitionGroup>
</div> </div>

View File

@ -15,7 +15,7 @@ function useAccess() {
* @param roles * @param roles
*/ */
function hasAuthByRoles(roles: string[]) { function hasAuthByRoles(roles: string[]) {
const userRoleSet = new Set(coreAccessStore.getUserRoles); const userRoleSet = new Set(coreAccessStore.userRoles);
const intersection = roles.filter((item) => userRoleSet.has(item)); const intersection = roles.filter((item) => userRoleSet.has(item));
return intersection.length > 0; return intersection.length > 0;
} }
@ -26,7 +26,7 @@ function useAccess() {
* @param codes * @param codes
*/ */
function hasAuthByCodes(codes: string[]) { function hasAuthByCodes(codes: string[]) {
const userCodesSet = new Set(coreAccessStore.getAccessCodes); const userCodesSet = new Set(coreAccessStore.accessCodes);
const intersection = codes.filter((item) => userCodesSet.has(item)); const intersection = codes.filter((item) => userCodesSet.has(item));
return intersection.length > 0; return intersection.length > 0;

View File

@ -10,12 +10,12 @@ import { useContentSpinner } from './use-content-spinner';
defineOptions({ name: 'LayoutContent' }); defineOptions({ name: 'LayoutContent' });
const tabsStore = useCoreTabbarStore(); const tabbarStore = useCoreTabbarStore();
const { keepAlive } = usePreferences(); const { keepAlive } = usePreferences();
const { spinning } = useContentSpinner(); const { spinning } = useContentSpinner();
const { getCacheTabs, getExcludeTabs, renderRouteView } = const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
storeToRefs(tabsStore); storeToRefs(tabbarStore);
// //
function getTransitionName(route: RouteLocationNormalizedLoaded) { function getTransitionName(route: RouteLocationNormalizedLoaded) {
@ -36,7 +36,7 @@ function getTransitionName(route: RouteLocationNormalizedLoaded) {
// return; // return;
// } // }
// 使 // 使
const inTabs = getCacheTabs.value.includes(route.name as string); const inTabs = getCachedTabs.value.includes(route.name as string);
return inTabs && route.meta.loaded ? undefined : transitionName; return inTabs && route.meta.loaded ? undefined : transitionName;
} }
@ -54,8 +54,8 @@ function getTransitionName(route: RouteLocationNormalizedLoaded) {
<Transition :name="getTransitionName(route)" appear mode="out-in"> <Transition :name="getTransitionName(route)" appear mode="out-in">
<KeepAlive <KeepAlive
v-if="keepAlive" v-if="keepAlive"
:exclude="getExcludeTabs" :exclude="getExcludeCachedTabs"
:include="getCacheTabs" :include="getCachedTabs"
> >
<component <component
:is="Component" :is="Component"

View File

@ -34,7 +34,7 @@ const { globalSearchShortcutKey } = usePreferences();
<div class="flex h-full min-w-0 flex-shrink-0 items-center"> <div class="flex h-full min-w-0 flex-shrink-0 items-center">
<GlobalSearch <GlobalSearch
:enable-shortcut-key="globalSearchShortcutKey" :enable-shortcut-key="globalSearchShortcutKey"
:menus="accessStore.getAccessMenus" :menus="accessStore.accessMenus"
class="mr-4" class="mr-4"
/> />
<ThemeToggle class="mr-2" /> <ThemeToggle class="mr-2" />

View File

@ -13,7 +13,7 @@ function useExtraMenu() {
const accessStore = useCoreAccessStore(); const accessStore = useCoreAccessStore();
const { navigation } = useNavigation(); const { navigation } = useNavigation();
const menus = computed(() => accessStore.getAccessMenus); const menus = computed(() => accessStore.accessMenus);
const route = useRoute(); const route = useRoute();
const extraMenus = ref<MenuRecordRaw[]>([]); const extraMenus = ref<MenuRecordRaw[]>([]);

View File

@ -29,7 +29,7 @@ function useMixedMenu() {
} }
return enableSidebar; return enableSidebar;
}); });
const menus = computed(() => accessStore.getAccessMenus); const menus = computed(() => accessStore.accessMenus);
/** /**
* *

View File

@ -1,4 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
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 { TabsView } from '@vben-core/tabs-ui';
import { useTabs } from './use-tabs'; import { useTabs } from './use-tabs';
@ -9,14 +13,23 @@ defineOptions({
defineProps<{ showIcon?: boolean }>(); defineProps<{ showIcon?: boolean }>();
const route = useRoute();
const coreTabbarStore = useCoreTabbarStore();
const { const {
createContextMenus, createContextMenus,
currentActive, currentActive,
currentTabs, currentTabs,
handleClick, handleClick,
handleClose, handleClose,
handleUnPushPin, handleUnpinTab,
} = useTabs(); } = useTabs();
// tabtab
if (!preferences.tabbar.persist) {
coreTabbarStore.closeOtherTabs(route);
}
</script> </script>
<template> <template>
@ -26,7 +39,7 @@ const {
:show-icon="showIcon" :show-icon="showIcon"
:tabs="currentTabs" :tabs="currentTabs"
@close="handleClose" @close="handleClose"
@un-push-pin="handleUnPushPin" @unpin-tab="handleUnpinTab"
@update:active="handleClick" @update:active="handleClick"
/> />
</template> </template>

View File

@ -30,7 +30,7 @@ function useTabs() {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const accessStore = useCoreAccessStore(); const accessStore = useCoreAccessStore();
const tabbarStore = useCoreTabbarStore(); const coreTabbarStore = useCoreTabbarStore();
const { accessMenus } = storeToRefs(accessStore); const { accessMenus } = storeToRefs(accessStore);
const currentActive = computed(() => { const currentActive = computed(() => {
@ -39,7 +39,7 @@ function useTabs() {
const { locale } = useI18n(); const { locale } = useI18n();
const currentTabs = ref<RouteLocationNormalizedGeneric[]>(); const currentTabs = ref<RouteLocationNormalizedGeneric[]>();
watch([() => tabbarStore.getTabs, () => locale.value], ([tabs, _]) => { watch([() => coreTabbarStore.getTabs, () => locale.value], ([tabs, _]) => {
currentTabs.value = tabs.map((item) => wrapperTabLocale(item)); currentTabs.value = tabs.map((item) => wrapperTabLocale(item));
}); });
@ -50,7 +50,7 @@ function useTabs() {
const affixTabs = filterTree(router.getRoutes(), (route) => { const affixTabs = filterTree(router.getRoutes(), (route) => {
return !!route.meta?.affixTab; return !!route.meta?.affixTab;
}); });
tabbarStore.setAffixTabs(affixTabs); coreTabbarStore.setAffixTabs(affixTabs);
}; };
// 点击tab,跳转路由 // 点击tab,跳转路由
@ -60,7 +60,7 @@ function useTabs() {
// 关闭tab // 关闭tab
const handleClose = async (key: string) => { const handleClose = async (key: string) => {
await tabbarStore.closeTabByKey(key, router); await coreTabbarStore.closeTabByKey(key, router);
}; };
function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) { function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {
@ -84,14 +84,14 @@ function useTabs() {
watch( watch(
() => route.path, () => route.path,
() => { () => {
tabbarStore.addTab(route as RouteLocationNormalized); coreTabbarStore.addTab(route as RouteLocationNormalized);
}, },
{ immediate: true }, { immediate: true },
); );
const createContextMenus = (tab: TabItem) => { const createContextMenus = (tab: TabItem) => {
const tabs = tabbarStore.getTabs; const tabs = coreTabbarStore.getTabs;
const affixTabs = tabbarStore.affixTabs; const affixTabs = coreTabbarStore.affixTabs;
const index = tabs.findIndex((item) => item.path === tab.path); const index = tabs.findIndex((item) => item.path === tab.path);
const disabled = tabs.length <= 1; const disabled = tabs.length <= 1;
@ -113,7 +113,7 @@ function useTabs() {
{ {
disabled: !isCurrentTab, disabled: !isCurrentTab,
handler: async () => { handler: async () => {
await tabbarStore.refresh(router); await coreTabbarStore.refresh(router);
}, },
icon: IcRoundRefresh, icon: IcRoundRefresh,
key: 'reload', key: 'reload',
@ -122,7 +122,7 @@ function useTabs() {
{ {
disabled: !!affixTab || disabled, disabled: !!affixTab || disabled,
handler: async () => { handler: async () => {
await tabbarStore.closeTab(tab, router); await coreTabbarStore.closeTab(tab, router);
}, },
icon: IcRoundClose, icon: IcRoundClose,
key: 'close', key: 'close',
@ -131,8 +131,8 @@ function useTabs() {
{ {
handler: async () => { handler: async () => {
await (affixTab await (affixTab
? tabbarStore.unPushPinTab(tab) ? coreTabbarStore.unpinTab(tab)
: tabbarStore.pushPinTab(tab)); : coreTabbarStore.pinTab(tab));
}, },
icon: affixTab ? MdiPinOff : MdiPin, icon: affixTab ? MdiPinOff : MdiPin,
key: 'affix', key: 'affix',
@ -144,7 +144,7 @@ function useTabs() {
{ {
disabled: closeLeftDisabled, disabled: closeLeftDisabled,
handler: async () => { handler: async () => {
await tabbarStore.closeLeftTabs(tab); await coreTabbarStore.closeLeftTabs(tab);
}, },
icon: MdiFormatHorizontalAlignLeft, icon: MdiFormatHorizontalAlignLeft,
key: 'close-left', key: 'close-left',
@ -153,7 +153,7 @@ function useTabs() {
{ {
disabled: closeRightDisabled, disabled: closeRightDisabled,
handler: async () => { handler: async () => {
await tabbarStore.closeRightTabs(tab); await coreTabbarStore.closeRightTabs(tab);
}, },
icon: MdiFormatHorizontalAlignRight, icon: MdiFormatHorizontalAlignRight,
key: 'close-right', key: 'close-right',
@ -163,7 +163,7 @@ function useTabs() {
{ {
disabled: closeOtherDisabled, disabled: closeOtherDisabled,
handler: async () => { handler: async () => {
await tabbarStore.closeOtherTabs(tab); await coreTabbarStore.closeOtherTabs(tab);
}, },
icon: MdiArrowExpandHorizontal, icon: MdiArrowExpandHorizontal,
key: 'close-other', key: 'close-other',
@ -172,7 +172,7 @@ function useTabs() {
{ {
disabled, disabled,
handler: async () => { handler: async () => {
await tabbarStore.closeAllTabs(router); await coreTabbarStore.closeAllTabs(router);
}, },
icon: IcRoundMultipleStop, icon: IcRoundMultipleStop,
key: 'close-all', key: 'close-all',
@ -190,8 +190,8 @@ function useTabs() {
/** /**
* *
*/ */
const handleUnPushPin = async (tab: TabItem) => { const handleUnpinTab = async (tab: TabItem) => {
await tabbarStore.unPushPinTab(tab); await coreTabbarStore.unpinTab(tab);
}; };
return { return {
@ -200,7 +200,7 @@ function useTabs() {
currentTabs, currentTabs,
handleClick, handleClick,
handleClose, handleClose,
handleUnPushPin, handleUnpinTab,
}; };
} }

View File

@ -11,7 +11,7 @@ import { useCoreTabbarStore } from '@vben-core/stores';
defineOptions({ name: 'IFrameRouterView' }); defineOptions({ name: 'IFrameRouterView' });
const spinningList = ref<boolean[]>([]); const spinningList = ref<boolean[]>([]);
const tabsStore = useCoreTabbarStore(); const coreTabbarStore = useCoreTabbarStore();
const route = useRoute(); const route = useRoute();
const enableTabbar = computed(() => preferences.tabbar.enable); const enableTabbar = computed(() => preferences.tabbar.enable);
@ -20,7 +20,7 @@ const iframeRoutes = computed(() => {
if (!enableTabbar.value) { if (!enableTabbar.value) {
return route.meta.iframeSrc ? [route] : []; return route.meta.iframeSrc ? [route] : [];
} }
return tabsStore.getTabs.filter((tab) => !!tab.meta?.iframeSrc); return coreTabbarStore.getTabs.filter((tab) => !!tab.meta?.iframeSrc);
}); });
const tabNames = computed( const tabNames = computed(
@ -36,7 +36,7 @@ function routeShow(tabItem: RouteLocationNormalized) {
function canRender(tabItem: RouteLocationNormalized) { function canRender(tabItem: RouteLocationNormalized) {
const { meta, name } = tabItem; const { meta, name } = tabItem;
if (!name || !tabsStore.renderRouteView) { if (!name || !coreTabbarStore.renderRouteView) {
return false; return false;
} }
@ -52,7 +52,7 @@ function canRender(tabItem: RouteLocationNormalized) {
) { ) {
return false; return false;
} }
return tabsStore.getTabs.some((tab) => tab.name === name); return coreTabbarStore.getTabs.some((tab) => tab.name === name);
} }
function hideLoading(index: number) { function hideLoading(index: number) {

View File

@ -11,6 +11,7 @@ defineProps<{ disabled?: boolean }>();
const tabbarEnable = defineModel<boolean>('tabbarEnable'); const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon'); const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
</script> </script>
<template> <template>
@ -20,4 +21,7 @@ const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable"> <SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }} {{ $t('preferences.tabbar.icon') }}
</SwitchItem> </SwitchItem>
<SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.persist') }}
</SwitchItem>
</template> </template>

View File

@ -96,6 +96,7 @@ const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
const tabbarEnable = defineModel<boolean>('tabbarEnable'); const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon'); const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const navigationStyleType = defineModel<NavigationStyleType>( const navigationStyleType = defineModel<NavigationStyleType>(
'navigationStyleType', 'navigationStyleType',
@ -341,6 +342,7 @@ async function handleReset() {
<Block :title="$t('preferences.tabbar.title')"> <Block :title="$t('preferences.tabbar.title')">
<Tabbar <Tabbar
v-model:tabbar-enable="tabbarEnable" v-model:tabbar-enable="tabbarEnable"
v-model:tabbar-persist="tabbarPersist"
v-model:tabbar-show-icon="tabbarShowIcon" v-model:tabbar-show-icon="tabbarShowIcon"
/> />
</Block> </Block>