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 { translateMenuTitleByPath } from '@/utils/menu-i18n'; import { isLoginRouteName } from '@/utils/login-portal'; import i18n from '@/locale'; 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 metaLocale = route.meta?.locale; if (typeof metaLocale === 'string' && metaLocale.trim()) { const translated = String(i18n.global.t(metaLocale.trim())); if (translated && translated !== metaLocale.trim()) { return translated; } } const byPath = translateMenuTitleByPath(route.path); if (byPath) { return byPath; } 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, 'H5Login', 'H5AdminLogin', 'siteRoot']; function shouldSkipTabRoute(route: RouteLocationNormalized): boolean { const name = route.name != null ? String(route.name) : ''; if (!name) return true; if (BAN_LIST.includes(name)) return true; if (isLoginRouteName(name)) return true; if (route.meta?.noAffix) return true; if (route.meta?.requiresAuth === false && (route.path === '/login' || route.path === '/admin')) { return true; } return false; } const useTabBarStore = defineStore('tabBar', { state: (): TabBarState => ({ cacheTabList: new Set([DEFAULT_ROUTE_NAME]), tagList: [DEFAULT_ROUTE], previewingPath: '', previewingName: '', routeRefreshTicks: {}, }), 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:访问时挪到末尾,并裁剪超量缓存。 * 必须一次性重赋值 Set,禁止先 delete 再 add: * keep-alive 会监听 include,名字从列表里消失会立刻 prune 掉缓存实例, * 表现为「切换标签就整页刷新」。 */ touchCache(name: string) { if (!isString(name) || name === '') return; const next = new Set( [...this.cacheTabList].filter((item) => item !== name), ); next.add(name); this.cacheTabList = next; this.pruneCache(name); }, updateTabList(route: RouteLocationNormalized) { if (shouldSkipTabRoute(route)) return; // 登录页曾被误写入时清掉,避免登录成功后仍挂着「login」页签 this.tagList = this.tagList.filter( (tag) => !isLoginRouteName(tag.name) && tag.name !== 'H5Login' && tag.name !== 'H5AdminLogin', ); const next = formatTag(route); const idx = this.tagList.findIndex( (tag) => tag.fullPath === next.fullPath || (!!next.name && tag.name === next.name), ); // 同路由仅 query 变化(如公告 tab 的 noticeType)时更新已有页签,避免顶栏重复开签 if (idx !== -1) { const prev = this.tagList[idx]; this.tagList[idx] = { ...prev, fullPath: next.fullPath, query: next.query, title: next.title || prev.title, ignoreCache: next.ignoreCache, }; } else { this.tagList.push(next); } if (!route.meta.ignoreCache) { this.touchCache(String(route.name)); } }, /** 菜单/标签点击立刻高亮,不等路由 afterEach */ previewNavigate(tag: TagProps) { if (!tag?.fullPath && !tag?.name) return; if ( isLoginRouteName(tag.name) || tag.name === 'H5Login' || tag.name === 'H5AdminLogin' ) { return; } this.previewingPath = tag.fullPath || ''; this.previewingName = tag.name || ''; const exists = this.tagList.some( (t) => (tag.fullPath && t.fullPath === tag.fullPath) || (tag.name && t.name === tag.name), ); if (!exists && tag.name) { this.tagList.push(tag); if (!tag.ignoreCache) { this.touchCache(tag.name); } } }, clearPreview() { if (!this.previewingPath && !this.previewingName) return; this.previewingPath = ''; this.previewingName = ''; }, 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); }, /** 顶栏刷新:仅 bump 一次 key,避免 router.replace 加/去 _r 导致页面挂载两次 */ bumpRouteRefresh(routeName: string) { const key = String(routeName || '').trim(); if (!key) return; const prev = this.routeRefreshTicks[key] || 0; this.routeRefreshTicks[key] = prev + 1; }, 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); this.previewingPath = ''; this.previewingName = ''; }, 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); }, /** 菜单加载或切换语言后刷新标签标题 */ refreshTagTitles() { const rbacUserStore = useRbacUserStore(); const menus = rbacUserStore.menus as MenuTreeDto[]; this.tagList = this.tagList.map((tag) => { const fromTree = resolveMenuTitleFromTree(menus, tag.fullPath, ''); if (fromTree) { return { ...tag, title: fromTree }; } const byPath = translateMenuTitleByPath(tag.fullPath); if (byPath) { return { ...tag, title: byPath }; } return tag; }); }, }, }); export default useTabBarStore;