tabbar.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import type { TabDefinition } from '@vben-core/typings';
  2. import type { Router, RouteRecordNormalized } from 'vue-router';
  3. import { toRaw } from 'vue';
  4. import { openWindow, startProgress, stopProgress } from '@vben-core/shared';
  5. import { acceptHMRUpdate, defineStore } from 'pinia';
  6. interface TabbarState {
  7. /**
  8. * @zh_CN 当前打开的标签页列表缓存
  9. */
  10. cachedTabs: Set<string>;
  11. /**
  12. * @zh_CN 拖拽结束的索引
  13. */
  14. dragEndIndex: number;
  15. /**
  16. * @zh_CN 需要排除缓存的标签页
  17. */
  18. excludeCachedTabs: Set<string>;
  19. /**
  20. * @zh_CN 是否刷新
  21. */
  22. renderRouteView?: boolean;
  23. /**
  24. * @zh_CN 当前打开的标签页列表
  25. */
  26. tabs: TabDefinition[];
  27. /**
  28. * @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能
  29. */
  30. updateTime?: number;
  31. }
  32. /**
  33. * @zh_CN 访问权限相关
  34. */
  35. export const useTabbarStore = defineStore('core-tabbar', {
  36. actions: {
  37. /**
  38. * Close tabs in bulk
  39. */
  40. async _bulkCloseByPaths(paths: string[]) {
  41. this.tabs = this.tabs.filter((item) => {
  42. return !paths.includes(getTabPath(item));
  43. });
  44. this.updateCacheTab();
  45. },
  46. /**
  47. * @zh_CN 关闭标签页
  48. * @param tab
  49. */
  50. _close(tab: TabDefinition) {
  51. const { fullPath } = tab;
  52. if (isAffixTab(tab)) {
  53. return;
  54. }
  55. const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
  56. index !== -1 && this.tabs.splice(index, 1);
  57. },
  58. /**
  59. * @zh_CN 跳转到默认标签页
  60. */
  61. async _goToDefaultTab(router: Router) {
  62. if (this.getTabs.length <= 0) {
  63. // TODO: 跳转首页
  64. return;
  65. }
  66. const firstTab = this.getTabs[0];
  67. await this._goToTab(firstTab, router);
  68. },
  69. /**
  70. * @zh_CN 跳转到标签页
  71. * @param tab
  72. */
  73. async _goToTab(tab: TabDefinition, router: Router) {
  74. const { params, path, query } = tab;
  75. const toParams = {
  76. params: params || {},
  77. path,
  78. query: query || {},
  79. };
  80. await router.replace(toParams);
  81. },
  82. /**
  83. * @zh_CN 添加标签页
  84. * @param routeTab
  85. */
  86. addTab(routeTab: TabDefinition) {
  87. const tab = cloneTab(routeTab);
  88. if (!isTabShown(tab)) {
  89. return;
  90. }
  91. const tabIndex = this.tabs.findIndex((tab) => {
  92. return getTabPath(tab) === getTabPath(routeTab);
  93. });
  94. if (tabIndex === -1) {
  95. // 获取动态路由打开数,超过 0 即代表需要控制打开数
  96. const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ??
  97. -1) as number;
  98. // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
  99. // 获取到已经打开的动态路由数, 判断是否大于某一个值
  100. if (
  101. maxNumOfOpenTab > 0 &&
  102. this.tabs.filter((tab) => tab.name === routeTab.name).length >=
  103. maxNumOfOpenTab
  104. ) {
  105. // 关闭第一个
  106. const index = this.tabs.findIndex(
  107. (item) => item.name === routeTab.name,
  108. );
  109. index !== -1 && this.tabs.splice(index, 1);
  110. }
  111. this.tabs.push(tab);
  112. } else {
  113. // 页面已经存在,不重复添加选项卡,只更新选项卡参数
  114. const currentTab = toRaw(this.tabs)[tabIndex];
  115. const mergedTab = { ...currentTab, ...tab };
  116. if (Reflect.has(currentTab.meta, 'affixTab')) {
  117. mergedTab.meta.affixTab = currentTab.meta.affixTab;
  118. }
  119. this.tabs.splice(tabIndex, 1, mergedTab);
  120. }
  121. this.updateCacheTab();
  122. },
  123. /**
  124. * @zh_CN 关闭所有标签页
  125. */
  126. async closeAllTabs(router: Router) {
  127. this.tabs = this.tabs.filter((tab) => isAffixTab(tab));
  128. await this._goToDefaultTab(router);
  129. this.updateCacheTab();
  130. },
  131. /**
  132. * @zh_CN 关闭左侧标签页
  133. * @param tab
  134. */
  135. async closeLeftTabs(tab: TabDefinition) {
  136. const index = this.tabs.findIndex(
  137. (item) => getTabPath(item) === getTabPath(tab),
  138. );
  139. if (index < 1) {
  140. return;
  141. }
  142. const leftTabs = this.tabs.slice(0, index);
  143. const paths: string[] = [];
  144. for (const item of leftTabs) {
  145. if (!isAffixTab(item)) {
  146. paths.push(getTabPath(item));
  147. }
  148. }
  149. await this._bulkCloseByPaths(paths);
  150. },
  151. /**
  152. * @zh_CN 关闭其他标签页
  153. * @param tab
  154. */
  155. async closeOtherTabs(tab: TabDefinition) {
  156. const closePaths = this.tabs.map((item) => getTabPath(item));
  157. const paths: string[] = [];
  158. for (const path of closePaths) {
  159. if (path !== tab.fullPath) {
  160. const closeTab = this.tabs.find((item) => getTabPath(item) === path);
  161. if (!closeTab) {
  162. continue;
  163. }
  164. if (!isAffixTab(closeTab)) {
  165. paths.push(getTabPath(closeTab));
  166. }
  167. }
  168. }
  169. await this._bulkCloseByPaths(paths);
  170. },
  171. /**
  172. * @zh_CN 关闭右侧标签页
  173. * @param tab
  174. */
  175. async closeRightTabs(tab: TabDefinition) {
  176. const index = this.tabs.findIndex(
  177. (item) => getTabPath(item) === getTabPath(tab),
  178. );
  179. if (index >= 0 && index < this.tabs.length - 1) {
  180. const rightTabs = this.tabs.slice(index + 1);
  181. const paths: string[] = [];
  182. for (const item of rightTabs) {
  183. if (!isAffixTab(item)) {
  184. paths.push(getTabPath(item));
  185. }
  186. }
  187. await this._bulkCloseByPaths(paths);
  188. }
  189. },
  190. /**
  191. * @zh_CN 关闭标签页
  192. * @param tab
  193. * @param router
  194. */
  195. async closeTab(tab: TabDefinition, router: Router) {
  196. const { currentRoute } = router;
  197. // 关闭不是激活选项卡
  198. if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
  199. this._close(tab);
  200. this.updateCacheTab();
  201. return;
  202. }
  203. const index = this.getTabs.findIndex(
  204. (item) => getTabPath(item) === getTabPath(currentRoute.value),
  205. );
  206. const before = this.getTabs[index - 1];
  207. const after = this.getTabs[index + 1];
  208. // 下一个tab存在,跳转到下一个
  209. if (after) {
  210. this._close(currentRoute.value);
  211. await this._goToTab(after, router);
  212. // 上一个tab存在,跳转到上一个
  213. } else if (before) {
  214. this._close(currentRoute.value);
  215. await this._goToTab(before, router);
  216. } else {
  217. console.error('Failed to close the tab; only one tab remains open.');
  218. }
  219. },
  220. /**
  221. * @zh_CN 通过key关闭标签页
  222. * @param key
  223. */
  224. async closeTabByKey(key: string, router: Router) {
  225. const index = this.tabs.findIndex((item) => getTabPath(item) === key);
  226. if (index === -1) {
  227. return;
  228. }
  229. await this.closeTab(this.tabs[index], router);
  230. },
  231. /**
  232. * 根据路径获取标签页
  233. * @param path
  234. */
  235. getTabByPath(path: string) {
  236. return this.getTabs.find(
  237. (item) => getTabPath(item) === path,
  238. ) as TabDefinition;
  239. },
  240. /**
  241. * @zh_CN 新窗口打开标签页
  242. * @param tab
  243. */
  244. async openTabInNewWindow(tab: TabDefinition) {
  245. const { hash, origin } = location;
  246. const path = tab.fullPath;
  247. const fullPath = path.startsWith('/') ? path : `/${path}`;
  248. const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
  249. openWindow(url, { target: '_blank' });
  250. },
  251. /**
  252. * @zh_CN 固定标签页
  253. * @param tab
  254. */
  255. async pinTab(tab: TabDefinition) {
  256. const index = this.tabs.findIndex(
  257. (item) => getTabPath(item) === getTabPath(tab),
  258. );
  259. if (index !== -1) {
  260. tab.meta.affixTab = true;
  261. // this.addTab(tab);
  262. this.tabs.splice(index, 1, tab);
  263. }
  264. },
  265. /**
  266. * 刷新标签页
  267. */
  268. async refresh(router: Router) {
  269. const { currentRoute } = router;
  270. const { name } = currentRoute.value;
  271. this.excludeCachedTabs.add(name as string);
  272. this.renderRouteView = false;
  273. startProgress();
  274. await new Promise((resolve) => setTimeout(resolve, 200));
  275. this.excludeCachedTabs.delete(name as string);
  276. this.renderRouteView = true;
  277. stopProgress();
  278. },
  279. /**
  280. * @zh_CN 重置标签页标题
  281. */
  282. async resetTabTitle(tab: TabDefinition) {
  283. if (!tab?.meta?.newTabTitle) {
  284. return;
  285. }
  286. const findTab = this.tabs.find(
  287. (item) => getTabPath(item) === getTabPath(tab),
  288. );
  289. if (findTab) {
  290. findTab.meta.newTabTitle = undefined;
  291. await this.updateCacheTab();
  292. }
  293. },
  294. /**
  295. * 设置固定标签页
  296. * @param tabs
  297. */
  298. setAffixTabs(tabs: RouteRecordNormalized[]) {
  299. for (const tab of tabs) {
  300. tab.meta.affixTab = true;
  301. this.addTab(routeToTab(tab));
  302. }
  303. },
  304. /**
  305. * @zh_CN 设置标签页标题
  306. * @param tab
  307. * @param title
  308. */
  309. async setTabTitle(tab: TabDefinition, title: string) {
  310. const findTab = this.tabs.find(
  311. (item) => getTabPath(item) === getTabPath(tab),
  312. );
  313. if (findTab) {
  314. findTab.meta.newTabTitle = title;
  315. await this.updateCacheTab();
  316. }
  317. },
  318. setUpdateTime() {
  319. this.updateTime = Date.now();
  320. },
  321. /**
  322. * @zh_CN 设置标签页顺序
  323. * @param oldIndex
  324. * @param newIndex
  325. */
  326. async sortTabs(oldIndex: number, newIndex: number) {
  327. const currentTab = this.tabs[oldIndex];
  328. this.tabs.splice(oldIndex, 1);
  329. this.tabs.splice(newIndex, 0, currentTab);
  330. this.dragEndIndex = this.dragEndIndex + 1;
  331. },
  332. /**
  333. * @zh_CN 切换固定标签页
  334. * @param tab
  335. */
  336. async toggleTabPin(tab: TabDefinition) {
  337. const affixTab = tab?.meta?.affixTab ?? false;
  338. await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));
  339. },
  340. /**
  341. * @zh_CN 取消固定标签页
  342. * @param tab
  343. */
  344. async unpinTab(tab: TabDefinition) {
  345. const index = this.tabs.findIndex(
  346. (item) => getTabPath(item) === getTabPath(tab),
  347. );
  348. if (index !== -1) {
  349. tab.meta.affixTab = false;
  350. // this.addTab(tab);
  351. this.tabs.splice(index, 1, tab);
  352. }
  353. },
  354. /**
  355. * 根据当前打开的选项卡更新缓存
  356. */
  357. async updateCacheTab() {
  358. const cacheMap = new Set<string>();
  359. for (const tab of this.tabs) {
  360. // 跳过不需要持久化的标签页
  361. const keepAlive = tab.meta?.keepAlive;
  362. if (!keepAlive) {
  363. continue;
  364. }
  365. tab.matched.forEach((t, i) => {
  366. if (i > 0) {
  367. cacheMap.add(t.name as string);
  368. }
  369. });
  370. const name = tab.name as string;
  371. cacheMap.add(name);
  372. }
  373. this.cachedTabs = cacheMap;
  374. },
  375. },
  376. getters: {
  377. affixTabs(): TabDefinition[] {
  378. const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
  379. return affixTabs.sort((a, b) => {
  380. const orderA = (a.meta?.affixTabOrder ?? 0) as number;
  381. const orderB = (b.meta?.affixTabOrder ?? 0) as number;
  382. return orderA - orderB;
  383. });
  384. },
  385. getCachedTabs(): string[] {
  386. return [...this.cachedTabs];
  387. },
  388. getExcludeCachedTabs(): string[] {
  389. return [...this.excludeCachedTabs];
  390. },
  391. getTabs(): TabDefinition[] {
  392. const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
  393. return [...this.affixTabs, ...normalTabs].filter(Boolean);
  394. },
  395. },
  396. persist: [
  397. // tabs不需要保存在localStorage
  398. {
  399. paths: ['tabs'],
  400. storage: sessionStorage,
  401. },
  402. ],
  403. state: (): TabbarState => ({
  404. cachedTabs: new Set(),
  405. dragEndIndex: 0,
  406. excludeCachedTabs: new Set(),
  407. renderRouteView: true,
  408. tabs: [],
  409. updateTime: Date.now(),
  410. }),
  411. });
  412. // 解决热更新问题
  413. const hot = import.meta.hot;
  414. if (hot) {
  415. hot.accept(acceptHMRUpdate(useTabbarStore, hot));
  416. }
  417. /**
  418. * @zh_CN 克隆路由,防止路由被修改
  419. * @param route
  420. */
  421. function cloneTab(route: TabDefinition): TabDefinition {
  422. if (!route) {
  423. return route;
  424. }
  425. const { matched, ...opt } = route;
  426. return {
  427. ...opt,
  428. matched: (matched
  429. ? matched.map((item) => ({
  430. meta: item.meta,
  431. name: item.name,
  432. path: item.path,
  433. }))
  434. : undefined) as RouteRecordNormalized[],
  435. };
  436. }
  437. /**
  438. * @zh_CN 是否是固定标签页
  439. * @param tab
  440. */
  441. function isAffixTab(tab: TabDefinition) {
  442. return tab?.meta?.affixTab ?? false;
  443. }
  444. /**
  445. * @zh_CN 是否显示标签
  446. * @param tab
  447. */
  448. function isTabShown(tab: TabDefinition) {
  449. return !tab.meta.hideInTab;
  450. }
  451. /**
  452. * @zh_CN 获取标签页路径
  453. * @param tab
  454. */
  455. function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
  456. return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
  457. }
  458. function routeToTab(route: RouteRecordNormalized) {
  459. return {
  460. meta: route.meta,
  461. name: route.name,
  462. path: route.path,
  463. } as TabDefinition;
  464. }