2024-05-19 21:20:42 +08:00
|
|
|
|
import { startProgress, stopProgress } from '@vben-core/toolkit';
|
|
|
|
|
|
import { TabItem } from '@vben-core/typings';
|
|
|
|
|
|
import type { RouteRecordNormalized, Router } from 'vue-router';
|
|
|
|
|
|
|
|
|
|
|
|
import { acceptHMRUpdate, defineStore } from 'pinia';
|
|
|
|
|
|
import { toRaw } from 'vue';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 克隆路由,防止路由被修改
|
|
|
|
|
|
* @param route
|
|
|
|
|
|
*/
|
|
|
|
|
|
function cloneTab(route: TabItem): TabItem {
|
|
|
|
|
|
if (!route) {
|
|
|
|
|
|
return route;
|
|
|
|
|
|
}
|
|
|
|
|
|
const { matched, ...opt } = route;
|
|
|
|
|
|
return {
|
|
|
|
|
|
...opt,
|
|
|
|
|
|
matched: (matched
|
|
|
|
|
|
? matched.map((item) => ({
|
|
|
|
|
|
meta: item.meta,
|
|
|
|
|
|
name: item.name,
|
|
|
|
|
|
path: item.path,
|
|
|
|
|
|
}))
|
|
|
|
|
|
: undefined) as RouteRecordNormalized[],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function routeToTab(route: RouteRecordNormalized) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
meta: route.meta,
|
|
|
|
|
|
name: route.name,
|
|
|
|
|
|
path: route.path,
|
|
|
|
|
|
} as unknown as TabItem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TabsState {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 固定的标签页列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
affixTabs: RouteRecordNormalized[];
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 当前打开的标签页列表缓存
|
|
|
|
|
|
*/
|
|
|
|
|
|
cacheTabs: Set<string>;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 需要排除缓存的标签页
|
|
|
|
|
|
*/
|
|
|
|
|
|
excludeCacheTabs: Set<string>;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 是否刷新
|
|
|
|
|
|
*/
|
|
|
|
|
|
renderRouteView?: boolean;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 当前打开的标签页列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
tabs: TabItem[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 访问权限相关
|
|
|
|
|
|
*/
|
|
|
|
|
|
const useTabsStore = defineStore('tabs', {
|
|
|
|
|
|
actions: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Close tabs in bulk
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _bulkCloseByPaths(paths: string[]) {
|
|
|
|
|
|
this.tabs = this.tabs.filter((item) => {
|
|
|
|
|
|
return !paths.includes(this.getTabPath(item));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.updateCacheTab();
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
_close(tab: TabItem) {
|
|
|
|
|
|
const { fullPath } = tab;
|
|
|
|
|
|
if (this._isAffixTab(tab)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
|
|
|
|
|
|
index !== -1 && this.tabs.splice(index, 1);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 跳转到默认标签页
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _goToDefaultTab(router: Router) {
|
|
|
|
|
|
if (this.getTabs.length <= 0) {
|
|
|
|
|
|
// TODO: 跳转首页
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const firstTab = this.getTabs[0];
|
|
|
|
|
|
await this._goToTab(firstTab, router);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 跳转到标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _goToTab(tab: TabItem, router: Router) {
|
|
|
|
|
|
const { params, path, query } = tab;
|
|
|
|
|
|
const toParams = {
|
|
|
|
|
|
params: params || {},
|
|
|
|
|
|
path,
|
|
|
|
|
|
query: query || {},
|
|
|
|
|
|
};
|
|
|
|
|
|
await router.replace(toParams);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 是否是固定标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
_isAffixTab(tab: TabItem) {
|
|
|
|
|
|
return tab?.meta?.affixTab ?? false;
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 添加标签页
|
|
|
|
|
|
* @param routeTab
|
|
|
|
|
|
*/
|
|
|
|
|
|
addTab(routeTab: TabItem) {
|
|
|
|
|
|
const tab = cloneTab(routeTab);
|
|
|
|
|
|
const { fullPath, meta, params, query } = tab;
|
|
|
|
|
|
if (meta?.hideInTab) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tabIndex = this.tabs.findIndex((tab) => {
|
|
|
|
|
|
return this.getTabPath(tab) === this.getTabPath(routeTab);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (tabIndex === -1) {
|
|
|
|
|
|
this.tabs.push(tab);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
|
|
|
|
|
const currentTab = toRaw(this.tabs)[tabIndex];
|
|
|
|
|
|
if (!currentTab) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
currentTab.params = params || currentTab.params;
|
|
|
|
|
|
currentTab.query = query || currentTab.query;
|
|
|
|
|
|
currentTab.fullPath = fullPath || currentTab.fullPath;
|
|
|
|
|
|
this.tabs.splice(tabIndex, 1, currentTab);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.updateCacheTab();
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭所有标签页
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeAllTabs(router: Router) {
|
|
|
|
|
|
this.tabs = this.tabs.filter((tab) => this._isAffixTab(tab));
|
|
|
|
|
|
await this._goToDefaultTab(router);
|
|
|
|
|
|
this.updateCacheTab();
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭左侧标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeLeftTabs(tab: TabItem) {
|
|
|
|
|
|
const index = this.tabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
const leftTabs = this.tabs.slice(0, index);
|
|
|
|
|
|
const paths: string[] = [];
|
|
|
|
|
|
for (const item of leftTabs) {
|
|
|
|
|
|
if (!this._isAffixTab(tab)) {
|
|
|
|
|
|
paths.push(this.getTabPath(item));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await this._bulkCloseByPaths(paths);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭其他标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeOtherTabs(tab: TabItem) {
|
|
|
|
|
|
const closePaths = this.tabs.map((item) => this.getTabPath(item));
|
|
|
|
|
|
|
|
|
|
|
|
const paths: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const path of closePaths) {
|
|
|
|
|
|
if (path !== tab.fullPath) {
|
|
|
|
|
|
const closeTab = this.tabs.find(
|
|
|
|
|
|
(item) => this.getTabPath(item) === path,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!closeTab) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._isAffixTab(tab)) {
|
|
|
|
|
|
paths.push(this.getTabPath(closeTab));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await this._bulkCloseByPaths(paths);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭右侧标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeRightTabs(tab: TabItem) {
|
|
|
|
|
|
const index = this.tabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (index >= 0 && index < this.tabs.length - 1) {
|
|
|
|
|
|
const rightTabs = this.tabs.slice(index + 1, this.tabs.length);
|
|
|
|
|
|
|
|
|
|
|
|
const paths: string[] = [];
|
|
|
|
|
|
for (const item of rightTabs) {
|
|
|
|
|
|
if (!this._isAffixTab(tab)) {
|
|
|
|
|
|
paths.push(this.getTabPath(item));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await this._bulkCloseByPaths(paths);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 关闭标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
* @param router
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeTab(tab: TabItem, router: Router) {
|
|
|
|
|
|
const { currentRoute } = router;
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭不是激活选项卡
|
|
|
|
|
|
if (this.getTabPath(currentRoute.value) !== this.getTabPath(tab)) {
|
|
|
|
|
|
this._close(tab);
|
|
|
|
|
|
this.updateCacheTab();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const index = this.getTabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === this.getTabPath(currentRoute.value),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const before = this.getTabs[index - 1];
|
|
|
|
|
|
const after = this.getTabs[index + 1];
|
|
|
|
|
|
|
|
|
|
|
|
// 下一个tab存在,跳转到下一个
|
|
|
|
|
|
if (after) {
|
|
|
|
|
|
this._close(currentRoute.value);
|
|
|
|
|
|
await this._goToTab(after, router);
|
|
|
|
|
|
// 上一个tab存在,跳转到上一个
|
|
|
|
|
|
} else if (before) {
|
|
|
|
|
|
this._close(currentRoute.value);
|
|
|
|
|
|
await this._goToTab(before, router);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('关闭标签页失败,当前只剩一个标签页。');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 通过key关闭标签页
|
|
|
|
|
|
* @param key
|
|
|
|
|
|
*/
|
|
|
|
|
|
async closeTabByKey(key: string, router: Router) {
|
|
|
|
|
|
const index = this.tabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === key,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.closeTab(this.tabs[index], router);
|
|
|
|
|
|
},
|
|
|
|
|
|
getTabPath(tab: RouteRecordNormalized | TabItem) {
|
2024-05-22 22:56:26 +08:00
|
|
|
|
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
|
2024-05-19 21:20:42 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 固定标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async pushPinTab(tab: TabItem) {
|
|
|
|
|
|
const index = this.tabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
this.tabs[index].meta.affixTab = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// TODO: 这里应该把tab从tbs中移除
|
|
|
|
|
|
this.affixTabs.push(tab as unknown as RouteRecordNormalized);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 刷新标签页
|
|
|
|
|
|
*/
|
|
|
|
|
|
async refreshTab(router: Router) {
|
|
|
|
|
|
const { currentRoute } = router;
|
|
|
|
|
|
const { name } = currentRoute.value;
|
|
|
|
|
|
|
|
|
|
|
|
this.excludeCacheTabs.add(name as string);
|
|
|
|
|
|
this.renderRouteView = false;
|
|
|
|
|
|
startProgress();
|
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
|
|
|
|
|
|
|
|
|
|
this.excludeCacheTabs.delete(name as string);
|
|
|
|
|
|
this.renderRouteView = true;
|
|
|
|
|
|
stopProgress();
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置固定标签页
|
|
|
|
|
|
* @param tabs
|
|
|
|
|
|
*/
|
|
|
|
|
|
setAffixTabs(tabs: RouteRecordNormalized[]) {
|
|
|
|
|
|
for (const tab of tabs) {
|
|
|
|
|
|
this.addTab(routeToTab(tab));
|
|
|
|
|
|
}
|
|
|
|
|
|
this.affixTabs = tabs;
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @zh_CN 取消固定标签页
|
|
|
|
|
|
* @param tab
|
|
|
|
|
|
*/
|
|
|
|
|
|
async unPushPinTab(tab: TabItem) {
|
|
|
|
|
|
const index = this.affixTabs.findIndex(
|
|
|
|
|
|
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
this.affixTabs[index].meta.affixTab = false;
|
|
|
|
|
|
this.affixTabs.splice(index, 1);
|
|
|
|
|
|
this.addTab(tab);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据当前打开的选项卡更新缓存
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateCacheTab() {
|
|
|
|
|
|
const cacheMap = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const tab of this.tabs) {
|
|
|
|
|
|
// 跳过不需要持久化的标签页
|
|
|
|
|
|
const keepAlive = tab.meta?.keepAlive;
|
|
|
|
|
|
if (!keepAlive) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
tab.matched.forEach((t, i) => {
|
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
|
cacheMap.add(t.name as string);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const name = tab.name as string;
|
|
|
|
|
|
cacheMap.add(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.cacheTabs = cacheMap;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
getters: {
|
|
|
|
|
|
getCacheTabs(): string[] {
|
|
|
|
|
|
return [...this.cacheTabs];
|
|
|
|
|
|
},
|
|
|
|
|
|
getExcludeTabs(): string[] {
|
|
|
|
|
|
return [...this.excludeCacheTabs];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getTabs(): TabItem[] {
|
|
|
|
|
|
const tabs: TabItem[] = [];
|
|
|
|
|
|
const affixTabPaths = new Set<string>();
|
|
|
|
|
|
for (const tab of this.affixTabs) {
|
|
|
|
|
|
if (!tab.meta.hideInTab) {
|
|
|
|
|
|
tabs.push(routeToTab(tab));
|
|
|
|
|
|
affixTabPaths.add(tab.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const tab of this.tabs) {
|
|
|
|
|
|
if (!affixTabPaths.has(tab.path) && !tab.meta.hideInTab) {
|
|
|
|
|
|
tabs.push(tab);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return tabs;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
persist: {
|
|
|
|
|
|
// 持久化
|
|
|
|
|
|
paths: [],
|
|
|
|
|
|
},
|
|
|
|
|
|
state: (): TabsState => ({
|
|
|
|
|
|
affixTabs: [],
|
|
|
|
|
|
cacheTabs: new Set(),
|
|
|
|
|
|
excludeCacheTabs: new Set(),
|
|
|
|
|
|
renderRouteView: true,
|
|
|
|
|
|
tabs: [],
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 解决热更新问题
|
|
|
|
|
|
const hot = import.meta.hot;
|
|
|
|
|
|
if (hot) {
|
|
|
|
|
|
hot.accept(acceptHMRUpdate(useTabsStore, hot));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export { useTabsStore };
|