右击 Tab 标签页显示下拉菜单项,例如:刷新当前页、关闭当前页、关闭左侧、关闭右侧、关闭其他、关闭所有等操作。
useTabs 实现
- 对外暴露方法
// src/hooks/web/useTabs.ts
enum TableActionEnum { REFRESH, CLOSE_ALL, CLOSE_LEFT, CLOSE_RIGHT, CLOSE_OTHER, CLOSE_CURRENT, CLOSE }
export function useTabs(_router?: Router) {
return {
refreshPage: () => handleTabAction(TableActionEnum.REFRESH),
closeAll: () => handleTabAction(TableActionEnum.CLOSE_ALL),
closeLeft: () => handleTabAction(TableActionEnum.CLOSE_LEFT),
closeRight: () => handleTabAction(TableActionEnum.CLOSE_RIGHT),
closeOther: () => handleTabAction(TableActionEnum.CLOSE_OTHER),
closeCurrent: () => handleTabAction(TableActionEnum.CLOSE_CURRENT),
close: (tab?: RouteLocationNormalized) => handleTabAction(TableActionEnum.CLOSE)
}
}
- handleTabAction 方法处理 tab 点击事件
// src/hooks/web/useTabs.ts
import { useRouter } from 'vue-router'
import { useMultipleTabStore } from '/@/store/modules/multipleTab'
export function useTabs(_router?: Router) {
const tabStore = useMultipleTabStore()
const router = _router || useRouter()
const { currentRoute } = router
// 当前 tab 栏标签
function getCurrentTab() {
const route = unref(currentRoute)
return tabStore.getTabList.find((item) => item.fullPath === route.fullPath)!
}
async function handleTabAction(action: TableActionEnum, tab?: RouteLocationNormalized) {
const currentTab = getCurrentTab()
switch (action) {
case TableActionEnum.REFRESH:
await tabStore.refreshPage(router)
break
case TableActionEnum.CLOSE_ALL:
await tabStore.closeAllTab(router)
break
case TableActionEnum.CLOSE_LEFT:
await tabStore.closeLeftTabs(currentTab, router)
break
case TableActionEnum.CLOSE_RIGHT:
await tabStore.closeRightTabs(currentTab, router)
break
case TableActionEnum.CLOSE_OTHER:
await tabStore.closeOtherTabs(currentTab, router)
break
case TableActionEnum.CLOSE_CURRENT:
case TableActionEnum.CLOSE:
await tabStore.closeTab(tab || currentTab, router)
break
}
}
}
- useMultipleTabStore 方法
声明一个 store 实例 useMultipleTabStore,用来存储打开标签页。
// src/store/modules/multipleTab.ts
import { defineStore } from 'pinia';
export const useMultipleTabStore = defineStore({
id: 'app-multiple-tab',
state: () => ({}),
getters: {},
actions: {},
})
- 定义 State 和 Getter
// src/store/modules/multipleTab.ts
import { defineStore } from 'pinia';
import { Persistent } from '/@/utils/cache/persistent';
export const useMultipleTabStore = defineStore({
id: 'app-multiple-tab',
state: (): MultipleTabState => ({
cacheTabList: new Set(),
// 优先加载本地缓存
tabList: cacheTab ? Persistent.getLocal(MULTIPLE_TABS_KEY) || [] : [],
lastDragEndIndex: 0,
}),
getters: {
// 获取路由列表标签
getTabList(): RouteLocationNormalized[] {
return this.tabList
},
// 获取带缓存标签路由名称列表
getCachedTabList(): string[] {
return Array.from(this.cacheTabList)
},
// 获取最后一次拖动标签的索引
getLastDragEndIndex(): number {
return this.lastDragEndIndex
}
},
})
- store 中多标签页处理方法
- 打开标签页
async addTab(route: RouteLocationNormalized) {
const { path, name, fullPath, params query, meta } = getRawRoute(route)
// 错误处理页面 登录 重定向 等页面
if (
path === PageEnum.ERROR_PAGE ||
path === PageEnum.BASE_LOGIN ||
!name ||
[REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)
) {
return;
}
let updateIndex = -1;
// 标签已存在,不在重复添加标签
const tabHasExits = this.tabList.some((tab, index) => {
updateIndex = index
return (tab.fullPath || tab.path) === (fullPath || path)
})
// 更新标签
if (tabHasExits) {
const curTab = toRaw(this.tabList)[updateIndex]
if (!curTab) {
return
}
curTab.params = params || curTab.params
curTab.query = query || curTab.query
curTab.fullPath = fullPath || curTab.fullPath
// 替换原有的标签页路由记录
this.tabList.splice(updateIndex, 1, curTab)
} else {
// 添加标签: 获取动态路由打开数,超过 0 即代表需要控制打开数
const dynamicLevel = meta?.dynamicLevel ?? -1
if (dynamicLevel > 0) {
// 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
// 首先获取到真实的路由,使用配置方式减少计算开销.
// const realName: string = path.match(/(\S*)\//)![1];
const realPath = meta?.realPath ?? '';
// 获取到已经打开的动态路由数, 判断是否大于某一个值
if (
this.tabList.filter((e) => e.meta?.realPath ?? '' === realPath).length >= dynamicLevel
) {
// 关闭第一个
const index = this.tabList.findIndex((item) => item.meta.realPath === realPath);
index !== -1 && this.tabList.splice(index, 1);
}
}
}
this.updateCacheTab()
// 使用本地存储持久化
cacheTab && Persistent.setLocal(MULTIPLE_TABS_KEY, this.tabList)
}
- 更新缓存
// 根据当前打开的选项卡更新缓存
async updateCacheTab() {
const cacheMap: Set<string> = new Set()
for (const tab of this.tabList) {
// 缓存
const needCache = !item.meta?.ignoreKeepAlive
if (!needCache) {
continue
}
const name = item.name as string
cacheMap.add(name)
}
// 存储路由记录的 Set 集合
this.cacheTabList = cacheMap
}
- 清空缓存
clearCacheTabs(): void {
this.cacheTabList = new Set()
}
- 关闭 Tab 操作
组件提供了很多关闭方法(所有、当前、左侧、右侧等)。关闭操作本质上就是将对应标签页路由信息从 tabList 中删除。
closeAllTab
关闭所有非 affix 的 tab,并跳转到首页。
async closeAllTab(router: Router) {
this.tabList = this.tabList.filter((item) => item?.meta?.affix ?? false)
// 清空缓存列表
this.clearCacheTabs()
// 跳转首页
this.goToPage(router)
}
goToPage (router: Router) {}
closeLeftTabs
关闭指定路由左侧(非固定)标签页。
async closeLeftTabs(route: RouteLocationNormalized, router: Router) {
// 获取标签页索引值
const index = this.tabList.findIndex((item) => item.path === route.path)
if (index > 0) {
const leftTabs = this.tabList.slice(0, index)
const pathList: string[] = []
for (const item of leftTabs) {
const affix = item?.meta?.affix ?? false
if (!affix) {
pathList.push(item.fullPath)
}
}
// 批量关闭列表路由
this.bulkCloseTabs(pathList)
}
this.updateCacheTab()
this.handleGotoPage(router)
}
// 批量关闭标签页
async bulkCloseTabs(pathList: string[]) {
this.tabList = this.tabList.filter(item => !pathList.includes(item.fullPath))
}
// 路由跳转
async handleGotoPage(router: Router) {
const go = useGo(router)
go(unref(router.currentRoute).path, true)
}
closeRightTabs
与 closeLeftTabs 逻辑差不多,关闭指定路由右侧(非固定)标签页。
async closeRightTabs(route: RouteLocationNormalized, router: Router) {
const index = this.tabList.findIndex((item) => item.fullPath === route.fullPath)
if (index >= 0 && index < this.tabList.length - 1) {
const rightTabs = this.tabList.slice(index + 1, this.tabList.length)
const pathList: string[] = [];
for (const item of rightTabs) {
const affix = item?.meta?.affix ?? false;
if (!affix) {
pathList.push(item.fullPath);
}
}
this.bulkCloseTabs(pathList);
}
this.updateCacheTab();
handleGotoPage(router);
}
closeOtherTabs
关闭指定路由之外的其他(非固定标签页)
async closeOtherTabs(route: RouteLocationNormalized, router: Router) {
const closePathList = this.tabList.map((item) => item.fullPath)
const pathList: string[] = [];
for (const path of closePathList) {
// 指定路由之外(非固定)标签页都会被删除
if (path !== route.fullPath) {
const closeItem = this.tabList.find(item => item.path === path)
if (!closeItem) {
continue;
}
const affix = closeItem?.meta?.affix ?? false;
if (!affix) {
pathList.push(closeItem.fullPath);
}
}
}
this.bulkCloseTabs(pathList);
this.updateCacheTab();
handleGotoPage(router);
}
closeTab
关闭指定标签页。
import { useUserStore } from '/@/store/modules/user'
async closeTab(tab: RouteLocationNormalized, router: Router) {
// 内部方法,关闭指定路由(非固定)标签页
const close = (route: RouteLocationNormalized) => {
const { fullPath, meta: { affix } = {} } = route
if (affix) {
return
}
const index = this.tabList.findIndex(item => item.fullPath === fullPath)
index !== -1 && this.tabList.splice(index, 1)
}
const { currentRoute, replace } = router
const { path } = unref(currentRoute)
// 判断关闭标签是否当前激活状态
if(path !== tab.path) {
// 直接关闭退出
close(tab)
return
}
// 关闭当前激活状态的标签页
let toTarget: RouteLocationRaw = {}
const index = this.tabList.findIndex(item => item.path === path)
// 关闭最左侧的标签
if (index === 0) {
// 只有一个标签,跳到主页,否则跳到右侧标签
if (this.tabList.length === 1) {
const userStore = useUserStore()
toTarget = userStore.getUserInfo.homePath || PageEnum.BASE_HOME
} else {
// 跳到右侧标签
const page = this.tabList[index + 1]
toTarget = getToTarget(page)
}
} else {
// 非最左侧标签,关闭后跳转到左侧标签页
const page = this.tabList[index - 1]
toTarget = getToTarget(page)
}
close(currentRoute.value)
// 导航不会留下历史记录
await replace(toTarget)
}
// 路由地址格式化处理
const getToTarget = (tabItem: ) => {
const { params, path, query } = tabItem
return { path, params: params || {}, query: query || {} }
}
refreshPage
刷新 tab 标签。
async refreshPage(router: Router) {
const { currentRoute } = router
const route = unref(currentRoute)
const findTab = this.getCachedTabList.find((item) => item === route.name)
if (findTab) {
this.cacheTabList.delete(findTab)
}
const redo = useRedo(router)
await redo()
},