tabbar.ts 13 KB

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