/** * 工作标签页状态管理模块 * * 提供多标签页功能的完整状态管理 * * ## 主要功能 * * - 标签页打开和关闭 * - 标签页固定和取消固定 * - 批量关闭(左侧、右侧、其他、全部) * - 标签页缓存管理(KeepAlive) * - 标签页标题自定义 * - 标签页路由验证 * - 动态路由参数处理 * * ## 使用场景 * * - 多标签页导航 * - 页面缓存控制 * - 标签页右键菜单 * - 固定常用页面 * - 批量关闭标签 * * ## 核心特性 * * - 智能标签页复用(同路由名称复用) * - 固定标签页保护(不可关闭) * - KeepAlive 缓存排除管理 * - 路由有效性验证 * - 首页自动保留 * * ## 持久化 * - 使用 localStorage 存储 * - 存储键:sys-v{version}-worktab * - 刷新页面保持标签状态 * * @module store/modules/worktab * @author Art Design Pro Team */ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { router } from '@/router' import { LocationQueryRaw, Router } from 'vue-router' import { WorkTab } from '@/types' import { useCommon } from '@/hooks/core/useCommon' import { useMenuStore } from '@/store/modules/menu' import { useSettingStore } from '@/store/modules/setting' import { getFirstSecondLevelMenuPath, isWorkbenchRoute } from '@/utils/navigation/route' import { shouldAddKeepAliveExclude } from '@/utils/navigation/keep-alive-exclude' interface WorktabState { current: Partial opened: WorkTab[] keepAliveExclude: string[] } /** * 工作台标签页管理 Store */ export const useWorktabStore = defineStore( 'worktabStore', () => { // 状态定义 const current = ref>({}) const opened = ref([]) const keepAliveExclude = ref([]) // 计算属性 const hasOpenedTabs = computed(() => opened.value.length > 0) const hasMultipleTabs = computed(() => opened.value.length > 1) const currentTabIndex = computed(() => current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1 ) /** * 查找标签页索引 */ const findTabIndex = (path: string): number => { return opened.value.findIndex((tab) => tab.path === path) } /** * 获取标签页 */ const getTab = (path: string): WorkTab | undefined => { return opened.value.find((tab) => tab.path === path) } /** * 检查标签页是否可关闭 */ const isTabClosable = (tab: WorkTab): boolean => { return !tab.fixedTab } /** * 过滤不应展示在 worktab 中的父级占位路由。 * `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/tenant`)。 */ const shouldIgnoreTab = (tab: Partial): boolean => { if (tab.path === '/system') { return true } if (isWorkbenchRoute(tab.path)) { return true } const title = String(tab.title ?? tab.customTitle ?? '') return title === '工作台' || title === 'menus.dashboard.console' } /** * 标签全部关闭后的默认落点:侧栏第一个二级菜单(无则回退首页路径) */ const resolveEmptyTabsPath = (): string => { const menuStore = useMenuStore() const { homePath } = useCommon() const fromMenu = getFirstSecondLevelMenuPath(menuStore.menuList) if (fromMenu) { return fromMenu } const home = homePath.value if (home && !isWorkbenchRoute(home)) { return home } return '/' } /** * 根据路径打开标签页(不依赖路由守卫 setWorktab,用于清空后同路径仍须保留一个标签的场景) */ const ensureTabForPath = (path: string): void => { if (!path || shouldIgnoreTab({ path })) { return } let resolved try { resolved = router.resolve(path) } catch { return } if (resolved.matched.length === 0 || resolved.meta?.isHideTab) { return } const { homePath } = useCommon() const { showWorkTab } = useSettingStore() if (!showWorkTab && resolved.path !== homePath.value) { return } const { meta, name, params, query } = resolved openTab({ title: meta.title as string, icon: meta.icon as string, path: resolved.path, name: name as string, keepAlive: meta.keepAlive as boolean, params, query: query as LocationQueryRaw, fixedTab: meta.fixedTab as boolean }) } /** * 标签全部关闭后,导航到默认页并确保至少保留一个标签 */ const navigateToDefaultWithTab = (): void => { const fallbackPath = resolveEmptyTabsPath() current.value = {} safeRouterPush({ path: fallbackPath }) ensureTabForPath(fallbackPath) } /** * 安全的路由跳转 */ const safeRouterPush = (tab: Partial): void => { if (!tab.path) { console.warn('尝试跳转到无效路径的标签页') return } try { router.push({ path: tab.path, query: tab.query as LocationQueryRaw }) } catch (error) { console.error('路由跳转失败:', error) } } /** * 打开或激活一个选项卡 */ const openTab = (tab: WorkTab): void => { if (!tab.path) { console.warn('尝试打开无效的标签页') return } if (shouldIgnoreTab(tab)) return // 从 keepAlive 排除列表中移除 if (tab.name) { removeKeepAliveExclude(tab.name) } // 默认:先按 name 复用(动态参数同页);worktabMulti 仅按 path 匹配以支持同 name 多开 let existingIndex = -1 if (!tab.worktabMulti && tab.name) { existingIndex = opened.value.findIndex((t) => t.name === tab.name) } if (existingIndex === -1) { existingIndex = findTabIndex(tab.path) } if (existingIndex === -1) { // 新增标签页 const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length const newTab = { ...tab } if (tab.fixedTab) { opened.value.splice(insertIndex, 0, newTab) } else { opened.value.push(newTab) } current.value = newTab } else { // 更新现有标签页(当动态路由参数或查询变更时,复用同一标签) const existingTab = opened.value[existingIndex] opened.value[existingIndex] = { ...existingTab, path: tab.path, params: tab.params, query: tab.query, title: tab.title || existingTab.title, fixedTab: tab.fixedTab ?? existingTab.fixedTab, keepAlive: tab.keepAlive ?? existingTab.keepAlive, worktabMulti: tab.worktabMulti ?? existingTab.worktabMulti, name: tab.name || existingTab.name, icon: tab.icon || existingTab.icon } current.value = opened.value[existingIndex] } } /** * 查找固定标签页的插入位置 */ const findFixedTabInsertIndex = (): number => { let insertIndex = 0 for (let i = 0; i < opened.value.length; i++) { if (opened.value[i].fixedTab) { insertIndex = i + 1 } else { break } } return insertIndex } /** * 关闭指定的选项卡 */ const removeTab = (path: string): void => { const targetTab = getTab(path) const targetIndex = findTabIndex(path) if (targetIndex === -1) { console.warn(`尝试关闭不存在的标签页: ${path}`) return } if (targetTab && !isTabClosable(targetTab)) { console.warn(`尝试关闭固定标签页: ${path}`) return } // 从标签页列表中移除 opened.value.splice(targetIndex, 1) // 处理缓存排除 if (targetTab?.name) { addKeepAliveExclude(targetTab) } // 如果关闭后无标签页,跳转默认页并保留一个标签 if (!hasOpenedTabs.value) { navigateToDefaultWithTab() return } // 如果关闭的是当前激活标签,需要激活其他标签 if (current.value.path === path) { const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex current.value = opened.value[newIndex] safeRouterPush(current.value) } } /** * 关闭左侧选项卡 */ const removeLeft = (path: string): void => { const targetIndex = findTabIndex(path) if (targetIndex === -1) { console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`) return } // 获取左侧可关闭的标签页 const leftTabs = opened.value.slice(0, targetIndex) const closableLeftTabs = leftTabs.filter(isTabClosable) if (closableLeftTabs.length === 0) { console.warn('左侧没有可关闭的标签页') return } // 标记为缓存排除 markTabsToRemove(closableLeftTabs) // 移除左侧可关闭的标签页 opened.value = opened.value.filter( (tab, index) => index >= targetIndex || !isTabClosable(tab) ) // 确保当前标签是激活状态 const targetTab = getTab(path) if (targetTab) { current.value = targetTab } } /** * 关闭右侧选项卡 */ const removeRight = (path: string): void => { const targetIndex = findTabIndex(path) if (targetIndex === -1) { console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`) return } // 获取右侧可关闭的标签页 const rightTabs = opened.value.slice(targetIndex + 1) const closableRightTabs = rightTabs.filter(isTabClosable) if (closableRightTabs.length === 0) { console.warn('右侧没有可关闭的标签页') return } // 标记为缓存排除 markTabsToRemove(closableRightTabs) // 移除右侧可关闭的标签页 opened.value = opened.value.filter( (tab, index) => index <= targetIndex || !isTabClosable(tab) ) // 确保当前标签是激活状态 const targetTab = getTab(path) if (targetTab) { current.value = targetTab } } /** * 关闭其他选项卡 */ const removeOthers = (path: string): void => { const targetTab = getTab(path) if (!targetTab) { console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`) return } // 获取其他可关闭的标签页 const otherTabs = opened.value.filter((tab) => tab.path !== path) const closableTabs = otherTabs.filter(isTabClosable) if (closableTabs.length === 0) { console.warn('没有其他可关闭的标签页') return } // 标记为缓存排除 markTabsToRemove(closableTabs) // 只保留当前标签和固定标签 opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab)) // 确保当前标签是激活状态 current.value = targetTab } /** * 关闭所有可关闭的标签页 */ const removeAll = (): void => { const closableTabs = opened.value.filter(isTabClosable) if (closableTabs.length === 0) { console.warn('没有可关闭的标签页') return } markTabsToRemove(closableTabs) opened.value = opened.value.filter((tab) => !isTabClosable(tab)) if (!hasOpenedTabs.value) { navigateToDefaultWithTab() return } const targetTab = opened.value[0] current.value = targetTab safeRouterPush(targetTab) } /** * 将指定选项卡添加到 keepAlive 排除列表中 * @param options.force 点击已打开标签切换时传 true,即使标签仍在 opened 中也排除缓存以强制重新挂载 */ const addKeepAliveExclude = (tab: WorkTab, options?: { force?: boolean }): void => { const name = tab.name if ( !shouldAddKeepAliveExclude({ tabName: name, keepAlive: tab.keepAlive, openedHasSameName: opened.value.some((t) => t.name === name), force: options?.force }) ) { return } if (name && !keepAliveExclude.value.includes(name)) { keepAliveExclude.value.push(name) } } /** * 从 keepAlive 排除列表中移除指定组件名称 */ const removeKeepAliveExclude = (name: string): void => { if (!name) return keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name) } /** * 将传入的一组选项卡的组件名称标记为排除缓存 */ const markTabsToRemove = (tabs: WorkTab[]): void => { const removePaths = new Set(tabs.map((t) => t.path)) tabs.forEach((tab) => { if (!tab.keepAlive || !tab.name) return const stillOpen = opened.value.some( (t) => t.name === tab.name && !removePaths.has(t.path) ) if (stillOpen) return if (!keepAliveExclude.value.includes(tab.name)) { keepAliveExclude.value.push(tab.name) } }) } /** * 切换指定标签页的固定状态 */ const toggleFixedTab = (path: string): void => { const targetIndex = findTabIndex(path) if (targetIndex === -1) { console.warn(`尝试切换不存在标签页的固定状态: ${path}`) return } const tab = { ...opened.value[targetIndex] } tab.fixedTab = !tab.fixedTab // 移除原位置 opened.value.splice(targetIndex, 1) if (tab.fixedTab) { // 固定标签插入到所有固定标签的末尾 const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab) const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex opened.value.splice(insertIndex, 0, tab) } else { // 非固定标签插入到所有固定标签后 const fixedCount = opened.value.filter((t) => t.fixedTab).length opened.value.splice(fixedCount, 0, tab) } // 更新当前标签引用 if (current.value.path === path) { current.value = tab } } /** * 验证工作台标签页的路由有效性 */ const validateWorktabs = (routerInstance: Router): void => { try { // 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径 const isTabRouteValid = (tab: Partial): boolean => { try { if (tab.name) { const routes = routerInstance.getRoutes() if (routes.some((r) => r.name === tab.name)) return true } if (tab.path) { const resolved = routerInstance.resolve({ path: tab.path, query: (tab.query as LocationQueryRaw) || undefined }) return resolved.matched.length > 0 } return false } catch { return false } } // 过滤出有效的标签页 const validTabs = opened.value.filter( (tab) => !shouldIgnoreTab(tab) && isTabRouteValid(tab) ) if (validTabs.length !== opened.value.length) { console.warn('发现无效的标签页路由,已自动清理') opened.value = validTabs } // 验证当前激活标签的有效性 const isCurrentValid = current.value && !shouldIgnoreTab(current.value) && isTabRouteValid(current.value) if (!isCurrentValid && validTabs.length > 0) { console.warn('当前激活标签无效,已自动切换') current.value = validTabs[0] } else if (!isCurrentValid) { current.value = {} } const activePath = routerInstance.currentRoute.value.path if (isWorkbenchRoute(activePath) || shouldIgnoreTab(current.value)) { const fallback = resolveEmptyTabsPath() if (fallback && fallback !== activePath) { safeRouterPush({ path: fallback }) } } } catch (error) { console.error('验证工作台标签页失败:', error) } } /** * 清空所有状态(用于登出等场景) */ const clearAll = (): void => { current.value = {} opened.value = [] keepAliveExclude.value = [] } /** * 获取状态快照(用于持久化存储) */ const getStateSnapshot = (): WorktabState => { return { current: { ...current.value }, opened: [...opened.value], keepAliveExclude: [...keepAliveExclude.value] } } /** * 获取标签页标题 */ const getTabTitle = (path: string): WorkTab | undefined => { const tab = getTab(path) return tab } /** * 更新标签页标题 */ const updateTabTitle = (path: string, title: string): void => { const tab = getTab(path) if (tab) { tab.customTitle = title } } /** * 重置标签页标题 */ const resetTabTitle = (path: string): void => { const tab = getTab(path) if (tab) { tab.customTitle = '' } } return { // 状态 current, opened, keepAliveExclude, // 计算属性 hasOpenedTabs, hasMultipleTabs, currentTabIndex, // 方法 ensureTabForPath, navigateToDefaultWithTab, openTab, removeTab, removeLeft, removeRight, removeOthers, removeAll, toggleFixedTab, validateWorktabs, clearAll, getStateSnapshot, // 工具方法 findTabIndex, getTab, isTabClosable, addKeepAliveExclude, removeKeepAliveExclude, markTabsToRemove, getTabTitle, updateTabTitle, resetTabTitle } }, { persist: { key: 'worktab', storage: localStorage } } )