Files
ruoyi-plus-vben5/packages/stores/src/modules/tabs.ts

400 lines
9.9 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 { 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) {
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
},
/**
* @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 };