176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
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,
|
||
}
|
||
})
|