tabbar.ts 13 KB

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