tabbar.ts 14 KB

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