tabbar.ts 17 KB

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