123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- import type { TabDefinition } from '@vben-core/typings';
- import type { Router, RouteRecordNormalized } from 'vue-router';
- import { toRaw } from 'vue';
- import { openWindow, startProgress, stopProgress } from '@vben-core/shared';
- import { acceptHMRUpdate, defineStore } from 'pinia';
- interface TabbarState {
- /**
- * @zh_CN 当前打开的标签页列表缓存
- */
- cachedTabs: Set<string>;
- /**
- * @zh_CN 拖拽结束的索引
- */
- dragEndIndex: number;
- /**
- * @zh_CN 需要排除缓存的标签页
- */
- excludeCachedTabs: Set<string>;
- /**
- * @zh_CN 是否刷新
- */
- renderRouteView?: boolean;
- /**
- * @zh_CN 当前打开的标签页列表
- */
- tabs: TabDefinition[];
- /**
- * @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能
- */
- updateTime?: number;
- }
- /**
- * @zh_CN 访问权限相关
- */
- export const useTabbarStore = defineStore('core-tabbar', {
- actions: {
- /**
- * Close tabs in bulk
- */
- async _bulkCloseByPaths(paths: string[]) {
- this.tabs = this.tabs.filter((item) => {
- return !paths.includes(getTabPath(item));
- });
- this.updateCacheTab();
- },
- /**
- * @zh_CN 关闭标签页
- * @param tab
- */
- _close(tab: TabDefinition) {
- const { fullPath } = tab;
- if (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: TabDefinition, router: Router) {
- const { params, path, query } = tab;
- const toParams = {
- params: params || {},
- path,
- query: query || {},
- };
- await router.replace(toParams);
- },
- /**
- * @zh_CN 添加标签页
- * @param routeTab
- */
- addTab(routeTab: TabDefinition) {
- const tab = cloneTab(routeTab);
- if (!isTabShown(tab)) {
- return;
- }
- const tabIndex = this.tabs.findIndex((tab) => {
- return getTabPath(tab) === getTabPath(routeTab);
- });
- if (tabIndex === -1) {
- // 获取动态路由打开数,超过 0 即代表需要控制打开数
- const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ??
- -1) as number;
- // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
- // 获取到已经打开的动态路由数, 判断是否大于某一个值
- if (
- maxNumOfOpenTab > 0 &&
- this.tabs.filter((tab) => tab.name === routeTab.name).length >=
- maxNumOfOpenTab
- ) {
- // 关闭第一个
- const index = this.tabs.findIndex(
- (item) => item.name === routeTab.name,
- );
- index !== -1 && this.tabs.splice(index, 1);
- }
- this.tabs.push(tab);
- } else {
- // 页面已经存在,不重复添加选项卡,只更新选项卡参数
- const currentTab = toRaw(this.tabs)[tabIndex];
- const mergedTab = { ...currentTab, ...tab };
- if (Reflect.has(currentTab.meta, 'affixTab')) {
- mergedTab.meta.affixTab = currentTab.meta.affixTab;
- }
- this.tabs.splice(tabIndex, 1, mergedTab);
- }
- this.updateCacheTab();
- },
- /**
- * @zh_CN 关闭所有标签页
- */
- async closeAllTabs(router: Router) {
- this.tabs = this.tabs.filter((tab) => isAffixTab(tab));
- await this._goToDefaultTab(router);
- this.updateCacheTab();
- },
- /**
- * @zh_CN 关闭左侧标签页
- * @param tab
- */
- async closeLeftTabs(tab: TabDefinition) {
- const index = this.tabs.findIndex(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (index < 1) {
- return;
- }
- const leftTabs = this.tabs.slice(0, index);
- const paths: string[] = [];
- for (const item of leftTabs) {
- if (!isAffixTab(item)) {
- paths.push(getTabPath(item));
- }
- }
- await this._bulkCloseByPaths(paths);
- },
- /**
- * @zh_CN 关闭其他标签页
- * @param tab
- */
- async closeOtherTabs(tab: TabDefinition) {
- const closePaths = this.tabs.map((item) => getTabPath(item));
- const paths: string[] = [];
- for (const path of closePaths) {
- if (path !== tab.fullPath) {
- const closeTab = this.tabs.find((item) => getTabPath(item) === path);
- if (!closeTab) {
- continue;
- }
- if (!isAffixTab(closeTab)) {
- paths.push(getTabPath(closeTab));
- }
- }
- }
- await this._bulkCloseByPaths(paths);
- },
- /**
- * @zh_CN 关闭右侧标签页
- * @param tab
- */
- async closeRightTabs(tab: TabDefinition) {
- const index = this.tabs.findIndex(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (index >= 0 && index < this.tabs.length - 1) {
- const rightTabs = this.tabs.slice(index + 1);
- const paths: string[] = [];
- for (const item of rightTabs) {
- if (!isAffixTab(item)) {
- paths.push(getTabPath(item));
- }
- }
- await this._bulkCloseByPaths(paths);
- }
- },
- /**
- * @zh_CN 关闭标签页
- * @param tab
- * @param router
- */
- async closeTab(tab: TabDefinition, router: Router) {
- const { currentRoute } = router;
- // 关闭不是激活选项卡
- if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
- this._close(tab);
- this.updateCacheTab();
- return;
- }
- const index = this.getTabs.findIndex(
- (item) => getTabPath(item) === 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('Failed to close the tab; only one tab remains open.');
- }
- },
- /**
- * @zh_CN 通过key关闭标签页
- * @param key
- */
- async closeTabByKey(key: string, router: Router) {
- const index = this.tabs.findIndex((item) => getTabPath(item) === key);
- if (index === -1) {
- return;
- }
- await this.closeTab(this.tabs[index], router);
- },
- /**
- * 根据路径获取标签页
- * @param path
- */
- getTabByPath(path: string) {
- return this.getTabs.find(
- (item) => getTabPath(item) === path,
- ) as TabDefinition;
- },
- /**
- * @zh_CN 新窗口打开标签页
- * @param tab
- */
- async openTabInNewWindow(tab: TabDefinition) {
- const { hash, origin } = location;
- const path = tab.fullPath;
- const fullPath = path.startsWith('/') ? path : `/${path}`;
- const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
- openWindow(url, { target: '_blank' });
- },
- /**
- * @zh_CN 固定标签页
- * @param tab
- */
- async pinTab(tab: TabDefinition) {
- const index = this.tabs.findIndex(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (index !== -1) {
- tab.meta.affixTab = true;
- // this.addTab(tab);
- this.tabs.splice(index, 1, tab);
- }
- },
- /**
- * 刷新标签页
- */
- async refresh(router: Router) {
- const { currentRoute } = router;
- const { name } = currentRoute.value;
- this.excludeCachedTabs.add(name as string);
- this.renderRouteView = false;
- startProgress();
- await new Promise((resolve) => setTimeout(resolve, 200));
- this.excludeCachedTabs.delete(name as string);
- this.renderRouteView = true;
- stopProgress();
- },
- /**
- * @zh_CN 重置标签页标题
- */
- async resetTabTitle(tab: TabDefinition) {
- if (!tab?.meta?.newTabTitle) {
- return;
- }
- const findTab = this.tabs.find(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (findTab) {
- findTab.meta.newTabTitle = undefined;
- await this.updateCacheTab();
- }
- },
- /**
- * 设置固定标签页
- * @param tabs
- */
- setAffixTabs(tabs: RouteRecordNormalized[]) {
- for (const tab of tabs) {
- tab.meta.affixTab = true;
- this.addTab(routeToTab(tab));
- }
- },
- /**
- * @zh_CN 设置标签页标题
- * @param tab
- * @param title
- */
- async setTabTitle(tab: TabDefinition, title: string) {
- const findTab = this.tabs.find(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (findTab) {
- findTab.meta.newTabTitle = title;
- await this.updateCacheTab();
- }
- },
- setUpdateTime() {
- this.updateTime = Date.now();
- },
- /**
- * @zh_CN 设置标签页顺序
- * @param oldIndex
- * @param newIndex
- */
- async sortTabs(oldIndex: number, newIndex: number) {
- const currentTab = this.tabs[oldIndex];
- this.tabs.splice(oldIndex, 1);
- this.tabs.splice(newIndex, 0, currentTab);
- this.dragEndIndex = this.dragEndIndex + 1;
- },
- /**
- * @zh_CN 切换固定标签页
- * @param tab
- */
- async toggleTabPin(tab: TabDefinition) {
- const affixTab = tab?.meta?.affixTab ?? false;
- await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));
- },
- /**
- * @zh_CN 取消固定标签页
- * @param tab
- */
- async unpinTab(tab: TabDefinition) {
- const index = this.tabs.findIndex(
- (item) => getTabPath(item) === getTabPath(tab),
- );
- if (index !== -1) {
- tab.meta.affixTab = false;
- // this.addTab(tab);
- this.tabs.splice(index, 1, 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.cachedTabs = cacheMap;
- },
- },
- getters: {
- affixTabs(): TabDefinition[] {
- const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
- return affixTabs.sort((a, b) => {
- const orderA = (a.meta?.affixTabOrder ?? 0) as number;
- const orderB = (b.meta?.affixTabOrder ?? 0) as number;
- return orderA - orderB;
- });
- },
- getCachedTabs(): string[] {
- return [...this.cachedTabs];
- },
- getExcludeCachedTabs(): string[] {
- return [...this.excludeCachedTabs];
- },
- getTabs(): TabDefinition[] {
- const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
- return [...this.affixTabs, ...normalTabs].filter(Boolean);
- },
- },
- persist: [
- // tabs不需要保存在localStorage
- {
- paths: ['tabs'],
- storage: sessionStorage,
- },
- ],
- state: (): TabbarState => ({
- cachedTabs: new Set(),
- dragEndIndex: 0,
- excludeCachedTabs: new Set(),
- renderRouteView: true,
- tabs: [],
- updateTime: Date.now(),
- }),
- });
- // 解决热更新问题
- const hot = import.meta.hot;
- if (hot) {
- hot.accept(acceptHMRUpdate(useTabbarStore, hot));
- }
- /**
- * @zh_CN 克隆路由,防止路由被修改
- * @param route
- */
- function cloneTab(route: TabDefinition): TabDefinition {
- 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[],
- };
- }
- /**
- * @zh_CN 是否是固定标签页
- * @param tab
- */
- function isAffixTab(tab: TabDefinition) {
- return tab?.meta?.affixTab ?? false;
- }
- /**
- * @zh_CN 是否显示标签
- * @param tab
- */
- function isTabShown(tab: TabDefinition) {
- return !tab.meta.hideInTab;
- }
- /**
- * @zh_CN 获取标签页路径
- * @param tab
- */
- function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
- return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
- }
- function routeToTab(route: RouteRecordNormalized) {
- return {
- meta: route.meta,
- name: route.name,
- path: route.path,
- } as TabDefinition;
- }
|