import type { RouteLocationNormalized } from 'vue-router'; import { defineStore } from 'pinia'; import { DEFAULT_ROUTE, DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME, } from '@/router/constants'; import { isString } from '@/utils/is'; import { useRbacUserStore } from '@/store/modules/rbac-user'; import { buildMenuTitleMaps, resolveMenuTitleByPath, resolveMenuTitleFromTree } from '@/utils/menu-display'; import type { MenuTreeDto } from '@/api/rbac'; import { TabBarState, TagProps } from './types'; /** keep-alive 同时缓存的页面上限(LRU),避免页签开太多越切越顿 */ export const MAX_KEEP_ALIVE_TABS = 10; function resolveRouteTabTitle(route: RouteLocationNormalized): string { const rbacUserStore = useRbacUserStore(); const { pathToTitle } = buildMenuTitleMaps(rbacUserStore.menus as MenuTreeDto[]); const menuTitle = resolveMenuTitleByPath(route.path, pathToTitle); if (menuTitle) { return menuTitle; } const metaTitle = route.meta?.title; if (typeof metaTitle === 'string' && metaTitle.trim()) { return metaTitle.trim(); } return String(route.name || route.path || ''); } const formatTag = (route: RouteLocationNormalized): TagProps => { const { name, meta, fullPath, query } = route; return { title: resolveRouteTabTitle(route), name: String(name), fullPath, query, ignoreCache: meta.ignoreCache, }; }; const BAN_LIST = [REDIRECT_ROUTE_NAME]; const useTabBarStore = defineStore('tabBar', { state: (): TabBarState => ({ cacheTabList: new Set([DEFAULT_ROUTE_NAME]), tagList: [DEFAULT_ROUTE], }), getters: { getTabList(): TagProps[] { return this.tagList; }, getCacheList(): string[] { return Array.from(this.cacheTabList); }, }, actions: { /** 超出上限时淘汰最久未访问的缓存(保留首页) */ pruneCache(exceptName?: string) { while (this.cacheTabList.size > MAX_KEEP_ALIVE_TABS) { let removed = false; for (const name of this.cacheTabList) { if (name === DEFAULT_ROUTE_NAME || name === exceptName) { continue; } this.cacheTabList.delete(name); removed = true; break; } if (!removed) break; } }, /** LRU:访问时挪到末尾,并裁剪超量缓存 */ touchCache(name: string) { if (!isString(name) || name === '') return; if (this.cacheTabList.has(name)) { this.cacheTabList.delete(name); } this.cacheTabList.add(name); this.pruneCache(name); }, updateTabList(route: RouteLocationNormalized) { if (BAN_LIST.includes(route.name as string)) return; this.tagList.push(formatTag(route)); if (!route.meta.ignoreCache) { this.touchCache(String(route.name)); } }, deleteTag(idx: number, tag: TagProps) { this.tagList.splice(idx, 1); this.cacheTabList.delete(tag.name); }, addCache(name: string) { if (isString(name) && name !== '') { this.touchCache(name); } }, deleteCache(tag: TagProps) { this.cacheTabList.delete(tag.name); }, freshTabList(tags: TagProps[]) { this.tagList = tags; this.cacheTabList.clear(); this.cacheTabList.add(DEFAULT_ROUTE_NAME); this.tagList .filter((el) => !el.ignoreCache) .map((el) => el.name) .forEach((x) => { if (x && x !== DEFAULT_ROUTE_NAME) { this.cacheTabList.add(x); } }); this.pruneCache(); }, resetTabList() { this.tagList = [DEFAULT_ROUTE]; this.cacheTabList.clear(); this.cacheTabList.add(DEFAULT_ROUTE_NAME); }, delVisitedRoute(path: string) { const idx = this.tagList.findIndex(tag => tag.fullPath === path); if (idx !== -1) { const tag = this.tagList[idx]; this.deleteTag(idx, tag); } }, /** 拖动排序标签:将 fromIndex 位置的标签移动到 toIndex */ reorderTabs(fromIndex: number, toIndex: number) { if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return; const list = [...this.tagList]; const [item] = list.splice(fromIndex, 1); list.splice(toIndex, 0, item); this.freshTabList(list); }, /** 菜单加载后刷新标签标题(使用 RBAC 菜单名称) */ refreshTagTitles() { const rbacUserStore = useRbacUserStore(); const menus = rbacUserStore.menus as MenuTreeDto[]; this.tagList = this.tagList.map((tag) => ({ ...tag, title: resolveMenuTitleFromTree(menus, tag.fullPath, tag.title) || tag.title, })); }, }, }); export default useTabBarStore;