tabbar.ts 16 KB

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