tabbar.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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 >= 0 && 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. tab.meta.affixTab = true;
  285. // this.addTab(tab);
  286. this.tabs.splice(index, 1, tab);
  287. }
  288. },
  289. /**
  290. * 刷新标签页
  291. */
  292. async refresh(router: Router) {
  293. const { currentRoute } = router;
  294. const { name } = currentRoute.value;
  295. this.excludeCachedTabs.add(name as string);
  296. this.renderRouteView = false;
  297. startProgress();
  298. await new Promise((resolve) => setTimeout(resolve, 200));
  299. this.excludeCachedTabs.delete(name as string);
  300. this.renderRouteView = true;
  301. stopProgress();
  302. },
  303. /**
  304. * @zh_CN 重置标签页标题
  305. */
  306. async resetTabTitle(tab: TabDefinition) {
  307. if (!tab?.meta?.newTabTitle) {
  308. return;
  309. }
  310. const findTab = this.tabs.find(
  311. (item) => getTabPath(item) === getTabPath(tab),
  312. );
  313. if (findTab) {
  314. findTab.meta.newTabTitle = undefined;
  315. await this.updateCacheTabs();
  316. }
  317. },
  318. /**
  319. * 设置固定标签页
  320. * @param tabs
  321. */
  322. setAffixTabs(tabs: RouteRecordNormalized[]) {
  323. for (const tab of tabs) {
  324. tab.meta.affixTab = true;
  325. this.addTab(routeToTab(tab));
  326. }
  327. },
  328. /**
  329. * @zh_CN 设置标签页标题
  330. * @param tab
  331. * @param title
  332. */
  333. async setTabTitle(tab: TabDefinition, title: string) {
  334. const findTab = this.tabs.find(
  335. (item) => getTabPath(item) === getTabPath(tab),
  336. );
  337. if (findTab) {
  338. findTab.meta.newTabTitle = title;
  339. await this.updateCacheTabs();
  340. }
  341. },
  342. setUpdateTime() {
  343. this.updateTime = Date.now();
  344. },
  345. /**
  346. * @zh_CN 设置标签页顺序
  347. * @param oldIndex
  348. * @param newIndex
  349. */
  350. async sortTabs(oldIndex: number, newIndex: number) {
  351. const currentTab = this.tabs[oldIndex];
  352. if (!currentTab) {
  353. return;
  354. }
  355. this.tabs.splice(oldIndex, 1);
  356. this.tabs.splice(newIndex, 0, currentTab);
  357. this.dragEndIndex = this.dragEndIndex + 1;
  358. },
  359. /**
  360. * @zh_CN 切换固定标签页
  361. * @param tab
  362. */
  363. async toggleTabPin(tab: TabDefinition) {
  364. const affixTab = tab?.meta?.affixTab ?? false;
  365. await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));
  366. },
  367. /**
  368. * @zh_CN 取消固定标签页
  369. * @param tab
  370. */
  371. async unpinTab(tab: TabDefinition) {
  372. const index = this.tabs.findIndex(
  373. (item) => getTabPath(item) === getTabPath(tab),
  374. );
  375. if (index !== -1) {
  376. tab.meta.affixTab = false;
  377. // this.addTab(tab);
  378. this.tabs.splice(index, 1, tab);
  379. }
  380. },
  381. /**
  382. * 根据当前打开的选项卡更新缓存
  383. */
  384. async updateCacheTabs() {
  385. const cacheMap = new Set<string>();
  386. for (const tab of this.tabs) {
  387. // 跳过不需要持久化的标签页
  388. const keepAlive = tab.meta?.keepAlive;
  389. if (!keepAlive) {
  390. continue;
  391. }
  392. (tab.matched || []).forEach((t, i) => {
  393. if (i > 0) {
  394. cacheMap.add(t.name as string);
  395. }
  396. });
  397. const name = tab.name as string;
  398. cacheMap.add(name);
  399. }
  400. this.cachedTabs = cacheMap;
  401. },
  402. },
  403. getters: {
  404. affixTabs(): TabDefinition[] {
  405. const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
  406. return affixTabs.sort((a, b) => {
  407. const orderA = (a.meta?.affixTabOrder ?? 0) as number;
  408. const orderB = (b.meta?.affixTabOrder ?? 0) as number;
  409. return orderA - orderB;
  410. });
  411. },
  412. getCachedTabs(): string[] {
  413. return [...this.cachedTabs];
  414. },
  415. getExcludeCachedTabs(): string[] {
  416. return [...this.excludeCachedTabs];
  417. },
  418. getTabs(): TabDefinition[] {
  419. const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
  420. return [...this.affixTabs, ...normalTabs].filter(Boolean);
  421. },
  422. },
  423. persist: [
  424. // tabs不需要保存在localStorage
  425. {
  426. pick: ['tabs'],
  427. storage: sessionStorage,
  428. },
  429. ],
  430. state: (): TabbarState => ({
  431. cachedTabs: new Set(),
  432. dragEndIndex: 0,
  433. excludeCachedTabs: new Set(),
  434. renderRouteView: true,
  435. tabs: [],
  436. updateTime: Date.now(),
  437. }),
  438. });
  439. // 解决热更新问题
  440. const hot = import.meta.hot;
  441. if (hot) {
  442. hot.accept(acceptHMRUpdate(useTabbarStore, hot));
  443. }
  444. /**
  445. * @zh_CN 克隆路由,防止路由被修改
  446. * @param route
  447. */
  448. function cloneTab(route: TabDefinition): TabDefinition {
  449. if (!route) {
  450. return route;
  451. }
  452. const { matched, meta, ...opt } = route;
  453. return {
  454. ...opt,
  455. matched: (matched
  456. ? matched.map((item) => ({
  457. meta: item.meta,
  458. name: item.name,
  459. path: item.path,
  460. }))
  461. : undefined) as RouteRecordNormalized[],
  462. meta: {
  463. ...meta,
  464. newTabTitle: meta.newTabTitle,
  465. },
  466. };
  467. }
  468. /**
  469. * @zh_CN 是否是固定标签页
  470. * @param tab
  471. */
  472. function isAffixTab(tab: TabDefinition) {
  473. return tab?.meta?.affixTab ?? false;
  474. }
  475. /**
  476. * @zh_CN 是否显示标签
  477. * @param tab
  478. */
  479. function isTabShown(tab: TabDefinition) {
  480. return !tab.meta.hideInTab;
  481. }
  482. /**
  483. * @zh_CN 获取标签页路径
  484. * @param tab
  485. */
  486. function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
  487. return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
  488. }
  489. function routeToTab(route: RouteRecordNormalized) {
  490. return {
  491. meta: route.meta,
  492. name: route.name,
  493. path: route.path,
  494. } as TabDefinition;
  495. }