admin-vben5/packages/stores/src/modules/tabbar.ts

622 lines
16 KiB
TypeScript
Raw Normal View History

import type { ComputedRef } from 'vue';
import type { Router, RouteRecordNormalized } from 'vue-router';
2024-06-08 19:49:06 +08:00
import type { TabDefinition } from '@vben-core/typings';
2024-06-08 19:49:06 +08:00
import { toRaw } from 'vue';
import { preferences } from '@vben-core/preferences';
import {
openRouteInNewWindow,
startProgress,
stopProgress,
} from '@vben-core/shared/utils';
2024-05-19 21:20:42 +08:00
import { acceptHMRUpdate, defineStore } from 'pinia';
2024-07-30 21:10:28 +08:00
interface TabbarState {
2024-05-19 21:20:42 +08:00
/**
* @zh_CN
*/
2024-07-10 00:50:41 +08:00
cachedTabs: Set<string>;
/**
* @zh_CN
*/
dragEndIndex: number;
2024-05-19 21:20:42 +08:00
/**
* @zh_CN
*/
2024-07-10 00:50:41 +08:00
excludeCachedTabs: Set<string>;
/**
* @zh_CN
*/
menuList: string[];
2024-05-19 21:20:42 +08:00
/**
* @zh_CN
*/
renderRouteView?: boolean;
/**
* @zh_CN
*/
tabs: TabDefinition[];
2024-07-20 09:36:10 +08:00
/**
* @zh_CN 使watch深度监听的话
*/
updateTime?: number;
2024-05-19 21:20:42 +08:00
}
/**
* @zh_CN 访
*/
2024-07-30 21:10:28 +08:00
export const useTabbarStore = defineStore('core-tabbar', {
2024-05-19 21:20:42 +08:00
actions: {
/**
* Close tabs in bulk
*/
async _bulkCloseByPaths(paths: string[]) {
this.tabs = this.tabs.filter((item) => {
2024-07-10 00:50:41 +08:00
return !paths.includes(getTabPath(item));
2024-05-19 21:20:42 +08:00
});
this.updateCacheTabs();
2024-05-19 21:20:42 +08:00
},
/**
* @zh_CN
* @param tab
*/
_close(tab: TabDefinition) {
2024-05-19 21:20:42 +08:00
const { fullPath } = tab;
if (isAffixTab(tab)) {
2024-05-19 21:20:42 +08:00
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) {
return;
}
const firstTab = this.getTabs[0];
if (firstTab) {
await this._goToTab(firstTab, router);
}
2024-05-19 21:20:42 +08:00
},
/**
* @zh_CN
* @param tab
* @param router
2024-05-19 21:20:42 +08:00
*/
async _goToTab(tab: TabDefinition, router: Router) {
2024-05-19 21:20:42 +08:00
const { params, path, query } = tab;
const toParams = {
params: params || {},
path,
query: query || {},
};
await router.replace(toParams);
},
/**
* @zh_CN
* @param routeTab
*/
addTab(routeTab: TabDefinition) {
2024-05-19 21:20:42 +08:00
const tab = cloneTab(routeTab);
2024-07-10 00:50:41 +08:00
if (!isTabShown(tab)) {
2024-05-19 21:20:42 +08:00
return;
}
const tabIndex = this.tabs.findIndex((tab) => {
2024-07-10 00:50:41 +08:00
return getTabPath(tab) === getTabPath(routeTab);
2024-05-19 21:20:42 +08:00
});
if (tabIndex === -1) {
const maxCount = preferences.tabbar.maxCount;
// 获取动态路由打开数,超过 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);
} else if (maxCount > 0 && this.tabs.length >= maxCount) {
// 关闭第一个
const index = this.tabs.findIndex(
(item) =>
!Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab,
);
index !== -1 && this.tabs.splice(index, 1);
}
2024-05-19 21:20:42 +08:00
this.tabs.push(tab);
} else {
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
const currentTab = toRaw(this.tabs)[tabIndex];
const mergedTab = {
...currentTab,
...tab,
meta: { ...currentTab?.meta, ...tab.meta },
};
if (currentTab) {
const curMeta = currentTab.meta;
if (Reflect.has(curMeta, 'affixTab')) {
mergedTab.meta.affixTab = curMeta.affixTab;
}
if (Reflect.has(curMeta, 'newTabTitle')) {
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
}
}
this.tabs.splice(tabIndex, 1, mergedTab);
2024-05-19 21:20:42 +08:00
}
this.updateCacheTabs();
2024-05-19 21:20:42 +08:00
},
/**
* @zh_CN
*/
async closeAllTabs(router: Router) {
const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
2024-05-19 21:20:42 +08:00
await this._goToDefaultTab(router);
this.updateCacheTabs();
2024-05-19 21:20:42 +08:00
},
/**
* @zh_CN
* @param tab
*/
async closeLeftTabs(tab: TabDefinition) {
2024-05-19 21:20:42 +08:00
const index = this.tabs.findIndex(
2024-07-10 00:50:41 +08:00
(item) => getTabPath(item) === getTabPath(tab),
2024-05-19 21:20:42 +08:00
);
2024-07-10 00:50:41 +08:00
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));
2024-05-19 21:20:42 +08:00
}
}
2024-07-10 00:50:41 +08:00
await this._bulkCloseByPaths(paths);
2024-05-19 21:20:42 +08:00
},
/**
* @zh_CN
* @param tab
*/
async closeOtherTabs(tab: TabDefinition) {
2024-07-10 00:50:41 +08:00
const closePaths = this.tabs.map((item) => getTabPath(item));
2024-05-19 21:20:42 +08:00
const paths: string[] = [];
for (const path of closePaths) {
if (path !== tab.fullPath) {
2024-07-10 00:50:41 +08:00
const closeTab = this.tabs.find((item) => getTabPath(item) === path);
2024-05-19 21:20:42 +08:00
if (!closeTab) {
continue;
}
2024-07-10 00:50:41 +08:00
if (!isAffixTab(closeTab)) {
paths.push(getTabPath(closeTab));
2024-05-19 21:20:42 +08:00
}
}
}
await this._bulkCloseByPaths(paths);
},
/**
* @zh_CN
* @param tab
*/
async closeRightTabs(tab: TabDefinition) {
2024-05-19 21:20:42 +08:00
const index = this.tabs.findIndex(
2024-07-10 00:50:41 +08:00
(item) => getTabPath(item) === getTabPath(tab),
2024-05-19 21:20:42 +08:00
);
chore(deps): bump the non-breaking-changes group with 3 updates (#4561) * chore(deps): bump the non-breaking-changes group with 3 updates Bumps the non-breaking-changes group with 3 updates: [@changesets/cli](https://github.com/changesets/changesets), [vue](https://github.com/vuejs/core) and [@vue/shared](https://github.com/vuejs/core/tree/HEAD/packages/shared). Updates `@changesets/cli` from 2.27.8 to 2.27.9 - [Release notes](https://github.com/changesets/changesets/releases) - [Changelog](https://github.com/changesets/changesets/blob/main/docs/modifying-changelog-format.md) - [Commits](https://github.com/changesets/changesets/compare/@changesets/cli@2.27.8...@changesets/cli@2.27.9) Updates `vue` from 3.5.10 to 3.5.11 - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) Updates `@vue/shared` from 3.5.10 to 3.5.11 - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/vuejs/core/commits/v3.5.11/packages/shared) --- updated-dependencies: - dependency-name: "@changesets/cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: non-breaking-changes - dependency-name: vue dependency-type: direct:production update-type: version-update:semver-patch dependency-group: non-breaking-changes - dependency-name: "@vue/shared" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: non-breaking-changes ... Signed-off-by: dependabot[bot] <support@github.com> * chore: update deps --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 09:52:04 +08:00
if (index !== -1 && index < this.tabs.length - 1) {
const rightTabs = this.tabs.slice(index + 1);
2024-05-19 21:20:42 +08:00
const paths: string[] = [];
for (const item of rightTabs) {
2024-07-10 00:50:41 +08:00
if (!isAffixTab(item)) {
paths.push(getTabPath(item));
2024-05-19 21:20:42 +08:00
}
}
await this._bulkCloseByPaths(paths);
}
},
/**
* @zh_CN
* @param tab
* @param router
*/
async closeTab(tab: TabDefinition, router: Router) {
2024-05-19 21:20:42 +08:00
const { currentRoute } = router;
// 关闭不是激活选项卡
2024-07-10 00:50:41 +08:00
if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
2024-05-19 21:20:42 +08:00
this._close(tab);
this.updateCacheTabs();
2024-05-19 21:20:42 +08:00
return;
}
const index = this.getTabs.findIndex(
2024-07-10 00:50:41 +08:00
(item) => getTabPath(item) === getTabPath(currentRoute.value),
2024-05-19 21:20:42 +08:00
);
const before = this.getTabs[index - 1];
const after = this.getTabs[index + 1];
// 下一个tab存在跳转到下一个
if (after) {
this._close(tab);
2024-05-19 21:20:42 +08:00
await this._goToTab(after, router);
// 上一个tab存在跳转到上一个
} else if (before) {
this._close(tab);
2024-05-19 21:20:42 +08:00
await this._goToTab(before, router);
} else {
2024-06-02 22:13:15 +08:00
console.error('Failed to close the tab; only one tab remains open.');
2024-05-19 21:20:42 +08:00
}
},
2024-05-19 21:20:42 +08:00
/**
* @zh_CN key关闭标签页
* @param key
* @param router
2024-05-19 21:20:42 +08:00
*/
async closeTabByKey(key: string, router: Router) {
const originKey = decodeURIComponent(key);
const index = this.tabs.findIndex(
(item) => getTabPath(item) === originKey,
);
2024-05-19 21:20:42 +08:00
if (index === -1) {
return;
}
const tab = this.tabs[index];
if (tab) {
await this.closeTab(tab, router);
}
2024-05-19 21:20:42 +08:00
},
/**
*
* @param path
*/
getTabByPath(path: string) {
return this.getTabs.find(
(item) => getTabPath(item) === path,
) as TabDefinition;
},
/**
* @zh_CN
* @param tab
*/
async openTabInNewWindow(tab: TabDefinition) {
openRouteInNewWindow(tab.fullPath || tab.path);
},
2024-05-19 21:20:42 +08:00
/**
* @zh_CN
* @param tab
*/
async pinTab(tab: TabDefinition) {
2024-05-19 21:20:42 +08:00
const index = this.tabs.findIndex(
2024-07-10 00:50:41 +08:00
(item) => getTabPath(item) === getTabPath(tab),
2024-05-19 21:20:42 +08:00
);
if (index !== -1) {
const oldTab = this.tabs[index];
2024-07-06 10:08:46 +08:00
tab.meta.affixTab = true;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
2024-07-06 10:08:46 +08:00
}
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index
const newIndex = affixTabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
// 交换位置重新排序
await this.sortTabs(index, newIndex);
2024-05-19 21:20:42 +08:00
},
2024-05-19 21:20:42 +08:00
/**
*
*/
async refresh(router: Router | string) {
// 如果是Router路由那么就根据当前路由刷新
// 如果是string字符串为路由名称则定向刷新指定标签页不能是当前路由名称否则不会刷新
if (typeof router === 'string') {
return await this.refreshByName(router);
}
2024-05-19 21:20:42 +08:00
const { currentRoute } = router;
const { name } = currentRoute.value;
2024-07-10 00:50:41 +08:00
this.excludeCachedTabs.add(name as string);
2024-05-19 21:20:42 +08:00
this.renderRouteView = false;
startProgress();
await new Promise((resolve) => setTimeout(resolve, 200));
2024-07-10 00:50:41 +08:00
this.excludeCachedTabs.delete(name as string);
2024-05-19 21:20:42 +08:00
this.renderRouteView = true;
stopProgress();
},
/**
*
*/
async refreshByName(name: string) {
this.excludeCachedTabs.add(name);
await new Promise((resolve) => setTimeout(resolve, 200));
this.excludeCachedTabs.delete(name);
},
/**
* @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.updateCacheTabs();
}
},
2024-05-19 21:20:42 +08:00
/**
*
* @param tabs
*/
setAffixTabs(tabs: RouteRecordNormalized[]) {
for (const tab of tabs) {
tab.meta.affixTab = true;
2024-05-19 21:20:42 +08:00
this.addTab(routeToTab(tab));
}
},
/**
* @zh_CN
* @param list
*/
setMenuList(list: string[]) {
this.menuList = list;
},
/**
* @zh_CN
*
* @zh_CN
* @zh_CN ,
* @zh_CN
*
* @param {TabDefinition} tab -
* @param {ComputedRef<string> | string} title - ,
*
* @example
* // 设置静态标题
* setTabTitle(tab, '新标签页');
*
* @example
* // 设置动态标题
* setTabTitle(tab, computed(() => t('common.dashboard')));
*/
async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {
const findTab = this.tabs.find(
(item) => getTabPath(item) === getTabPath(tab),
);
2024-07-20 09:36:10 +08:00
if (findTab) {
findTab.meta.newTabTitle = title;
2024-07-20 09:36:10 +08:00
await this.updateCacheTabs();
}
},
setUpdateTime() {
2024-07-20 09:36:10 +08:00
this.updateTime = Date.now();
},
/**
* @zh_CN
* @param oldIndex
* @param newIndex
*/
async sortTabs(oldIndex: number, newIndex: number) {
const currentTab = this.tabs[oldIndex];
if (!currentTab) {
return;
}
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));
},
2024-05-19 21:20:42 +08:00
/**
* @zh_CN
* @param tab
*/
async unpinTab(tab: TabDefinition) {
const index = this.tabs.findIndex(
2024-07-10 00:50:41 +08:00
(item) => getTabPath(item) === getTabPath(tab),
2024-05-19 21:20:42 +08:00
);
if (index !== -1) {
const oldTab = this.tabs[index];
2024-07-06 10:08:46 +08:00
tab.meta.affixTab = false;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
2024-05-19 21:20:42 +08:00
}
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
const newIndex = affixTabs.length;
// 交换位置重新排序
await this.sortTabs(index, newIndex);
2024-05-19 21:20:42 +08:00
},
/**
*
*/
async updateCacheTabs() {
2024-05-19 21:20:42 +08:00
const cacheMap = new Set<string>();
for (const tab of this.tabs) {
// 跳过不需要持久化的标签页
const keepAlive = tab.meta?.keepAlive;
if (!keepAlive) {
continue;
}
(tab.matched || []).forEach((t, i) => {
2024-05-19 21:20:42 +08:00
if (i > 0) {
cacheMap.add(t.name as string);
}
});
const name = tab.name as string;
cacheMap.add(name);
}
2024-07-10 00:50:41 +08:00
this.cachedTabs = cacheMap;
2024-05-19 21:20:42 +08:00
},
},
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;
});
},
2024-07-10 00:50:41 +08:00
getCachedTabs(): string[] {
return [...this.cachedTabs];
2024-05-19 21:20:42 +08:00
},
2024-07-10 00:50:41 +08:00
getExcludeCachedTabs(): string[] {
return [...this.excludeCachedTabs];
2024-05-19 21:20:42 +08:00
},
getMenuList(): string[] {
return this.menuList;
},
getTabs(): TabDefinition[] {
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
return [...this.affixTabs, ...normalTabs].filter(Boolean);
2024-05-19 21:20:42 +08:00
},
},
2024-07-12 22:23:41 +08:00
persist: [
// tabs不需要保存在localStorage
{
pick: ['tabs'],
2024-07-12 22:23:41 +08:00
storage: sessionStorage,
},
],
2024-07-30 21:10:28 +08:00
state: (): TabbarState => ({
2024-07-10 00:50:41 +08:00
cachedTabs: new Set(),
dragEndIndex: 0,
2024-07-10 00:50:41 +08:00
excludeCachedTabs: new Set(),
menuList: [
'close',
'affix',
'maximize',
'reload',
'open-in-new-window',
'close-left',
'close-right',
'close-other',
'close-all',
],
2024-05-19 21:20:42 +08:00
renderRouteView: true,
tabs: [],
2024-07-20 09:36:10 +08:00
updateTime: Date.now(),
2024-05-19 21:20:42 +08:00
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
2024-07-30 21:10:28 +08:00
hot.accept(acceptHMRUpdate(useTabbarStore, hot));
2024-05-19 21:20:42 +08:00
}
2024-07-10 00:50:41 +08:00
/**
* @zh_CN ,
* @param route
*/
function cloneTab(route: TabDefinition): TabDefinition {
2024-07-10 00:50:41 +08:00
if (!route) {
return route;
}
const { matched, meta, ...opt } = route;
2024-07-10 00:50:41 +08:00
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path,
}))
: undefined) as RouteRecordNormalized[],
meta: {
...meta,
newTabTitle: meta.newTabTitle,
},
2024-07-10 00:50:41 +08:00
};
}
/**
* @zh_CN
* @param tab
*/
function isAffixTab(tab: TabDefinition) {
return tab?.meta?.affixTab ?? false;
2024-07-10 00:50:41 +08:00
}
/**
* @zh_CN
* @param tab
*/
function isTabShown(tab: TabDefinition) {
const matched = tab?.matched ?? [];
return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab);
2024-07-10 00:50:41 +08:00
}
/**
* @zh_CN
* @param tab
*/
function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
2024-07-10 00:50:41 +08:00
}
function routeToTab(route: RouteRecordNormalized) {
return {
meta: route.meta,
name: route.name,
path: route.path,
} as TabDefinition;
2024-07-10 00:50:41 +08:00
}