import { defineStore } from 'pinia' import { ref } from 'vue' import type { RouteLocationNormalized } from 'vue-router' /** * 标签页项 * * 每个 tab 对应一个已打开的路由页面,path 作为唯一标识。 * 详情页带 query 参数时,同一 path 视为同一 tab(刷新 query,不重复开)。 */ export interface TabItem { /** 路由路径(唯一标识,不含 query) */ path: string /** 路由 name */ name: string /** 显示标题(取自 route.meta.title) */ title: string /** 图标(Element Plus Icon 名或 FontAwesome 类名) */ icon?: string /** 是否可关闭(首页 Dashboard 不可关闭) */ closable: boolean /** 路由 query(详情页带参数时保留,用于关闭后恢复) */ query: Record /** 完整 fullPath(含 query,用于路由跳转恢复) */ fullPath: string /** 组件是否已挂载(首次打开后置 true,常驻 DOM 不销毁,切换仅 v-show) */ loaded: boolean } /** * 多标签页状态管理 * * 对齐原生 GERP 的多 tab 切换能力: * - 点击菜单自动开 tab(重复点击激活已有,不重复开) * - tab 可关闭、可切换、右键菜单(关闭其他/全部关闭/刷新) * - 首页 Dashboard 常驻不可关闭 * - 不持久化(刷新后重置为仅首页,避免脏状态) */ export const useTabsStore = defineStore('tabs', () => { /** 首页常驻 tab */ const HOME_TAB: TabItem = { path: '/dashboard', name: 'Dashboard', title: '仪表盘', icon: 'Odometer', closable: false, query: {}, fullPath: '/dashboard', loaded: true, } /** 已打开的标签页列表 */ const tabs = ref([{ ...HOME_TAB }]) /** 当前激活的 tab path */ const activePath = ref('/dashboard') /** 各 tab 的刷新版本号(refreshTab 时递增,驱动组件重挂载) */ const refreshVersions = ref>({}) /** 判断图标是否 FontAwesome 类名(与 DefaultLayout.isFaIcon 一致) */ function isFaIcon(icon: string | undefined | null): boolean { if (!icon) return false const t = icon.trim() return t.includes(' ') || t.startsWith('fa-') || /^fa[brsl]?\b/.test(t) } /** * 新增/激活 tab(路由 beforeEach 调用) * - 已存在:更新 query/fullPath/title(详情页带不同参数时刷新),返回 false * - 不存在:追加新 tab(loaded=true 立即挂载组件)并激活,返回 true * @returns true=新 tab(首次打开,需 loading);false=已存在(已缓存,瞬时切换) */ function addTab(route: RouteLocationNormalized): boolean { const title = (route.meta.title as string) || (route.name as string) || route.path const icon = route.meta.icon as string | undefined const path = route.path const fullPath = route.fullPath // 提取 string 类型的 query const query: Record = {} for (const k of Object.keys(route.query)) { const v = route.query[k] if (typeof v === 'string') query[k] = v } const existing = tabs.value.find((t) => t.path === path) if (existing) { existing.query = query existing.fullPath = fullPath existing.title = title activePath.value = path return false } tabs.value.push({ path, name: String(route.name || ''), title, icon, closable: path !== '/dashboard', query, fullPath, loaded: true, }) activePath.value = path return true } /** * 关闭指定 tab * @returns 若关闭的是当前激活 tab,返回应跳转的 fullPath;否则返回 null */ function removeTab(path: string): string | null { const idx = tabs.value.findIndex((t) => t.path === path) if (idx === -1) return null if (!tabs.value[idx].closable) return null // 不可关闭 tabs.value.splice(idx, 1) // 若关闭的是当前激活 tab,激活相邻 tab if (activePath.value === path) { const next = tabs.value[idx] || tabs.value[idx - 1] || tabs.value[tabs.value.length - 1] if (next) { activePath.value = next.path return next.fullPath } } return null } /** 关闭其他所有可关闭 tab(保留首页与当前) */ function removeOthers(path: string): void { tabs.value = tabs.value.filter((t) => !t.closable || t.path === path) activePath.value = path } /** 关闭所有可关闭 tab(仅保留首页) */ function removeAll(): void { tabs.value = tabs.value.filter((t) => !t.closable) activePath.value = '/dashboard' } /** 获取 tab 当前刷新版本号(不存在则初始化为 0) */ function getRefreshVersion(path: string): number { if (!(path in refreshVersions.value)) refreshVersions.value[path] = 0 return refreshVersions.value[path] } /** * 刷新指定 tab:递增版本号,驱动 DefaultLayout 中组件 key 变化强制重挂载 * (v-show 常驻方案下组件不随 tab 切换重挂载,刷新只能靠 key 变化重建) */ function refreshTab(path: string): void { refreshVersions.value[path] = (refreshVersions.value[path] ?? 0) + 1 } /** 重置(登出时调用) */ function reset(): void { tabs.value = [{ ...HOME_TAB }] activePath.value = '/dashboard' refreshVersions.value = {} } return { tabs, activePath, refreshVersions, addTab, removeTab, removeOthers, removeAll, getRefreshVersion, refreshTab, reset, isFaIcon, } })