Files
GERP/src/stores/tabs.ts
T

176 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string>
/** 完整 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<TabItem[]>([{ ...HOME_TAB }])
/** 当前激活的 tab path */
const activePath = ref('/dashboard')
/** 各 tab 的刷新版本号(refreshTab 时递增,驱动组件重挂载) */
const refreshVersions = ref<Record<string, number>>({})
/** 判断图标是否 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<string, string> = {}
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,
}
})