tabbar.ts 15 KB

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