worktab.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /**
  2. * 工作标签页状态管理模块
  3. *
  4. * 提供多标签页功能的完整状态管理
  5. *
  6. * ## 主要功能
  7. *
  8. * - 标签页打开和关闭
  9. * - 标签页固定和取消固定
  10. * - 批量关闭(左侧、右侧、其他、全部)
  11. * - 标签页缓存管理(KeepAlive)
  12. * - 标签页标题自定义
  13. * - 标签页路由验证
  14. * - 动态路由参数处理
  15. *
  16. * ## 使用场景
  17. *
  18. * - 多标签页导航
  19. * - 页面缓存控制
  20. * - 标签页右键菜单
  21. * - 固定常用页面
  22. * - 批量关闭标签
  23. *
  24. * ## 核心特性
  25. *
  26. * - 智能标签页复用(同路由名称复用)
  27. * - 固定标签页保护(不可关闭)
  28. * - KeepAlive 缓存排除管理
  29. * - 路由有效性验证
  30. * - 同时最多打开 10 个标签(超出时自动关闭最早的可关闭标签)
  31. * - 首页自动保留
  32. *
  33. * ## 持久化
  34. * - 使用 localStorage 存储
  35. * - 存储键:sys-v{version}-worktab
  36. * - 刷新页面保持标签状态
  37. *
  38. * @module store/modules/worktab
  39. * @author Art Design Pro Team
  40. */
  41. import { defineStore } from 'pinia'
  42. import { ref, computed } from 'vue'
  43. import { HOME_PAGE_PATH, router } from '@/router'
  44. import { LocationQueryRaw, Router } from 'vue-router'
  45. import { WorkTab } from '@/types'
  46. import { MenuTypeEnum } from '@/enums/appEnum'
  47. import { useCommon } from '@/hooks/core/useCommon'
  48. import { useMenuStore } from '@/store/modules/menu'
  49. import { isWorkbenchRoute } from '@/utils/navigation/route'
  50. import { getPortalDefaultHomePath, getPortalDefaultPathForRoute } from '@/utils/router/portal-menu'
  51. import { useSettingStore } from '@/store/modules/setting'
  52. export type OpenTabOptions = { activate?: boolean }
  53. interface WorktabState {
  54. current: Partial<WorkTab>
  55. opened: WorkTab[]
  56. keepAliveExclude: string[]
  57. }
  58. /** 同时打开的工作标签页数量上限(固定标签不计入淘汰,但计入总数) */
  59. const MAX_OPEN_TABS = 10
  60. /**
  61. * 工作台标签页管理 Store
  62. */
  63. export const useWorktabStore = defineStore(
  64. 'worktabStore',
  65. () => {
  66. // 状态定义
  67. const current = ref<Partial<WorkTab>>({})
  68. const opened = ref<WorkTab[]>([])
  69. const keepAliveExclude = ref<string[]>([])
  70. // 计算属性
  71. const hasOpenedTabs = computed(() => opened.value.length > 0)
  72. const hasMultipleTabs = computed(() => opened.value.length > 1)
  73. const currentTabIndex = computed(() =>
  74. current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1
  75. )
  76. /**
  77. * 查找标签页索引
  78. */
  79. const findTabIndex = (path: string): number => {
  80. return opened.value.findIndex((tab) => tab.path === path)
  81. }
  82. /**
  83. * 获取标签页
  84. */
  85. const getTab = (path: string): WorkTab | undefined => {
  86. return opened.value.find((tab) => tab.path === path)
  87. }
  88. /**
  89. * 检查标签页是否可关闭
  90. */
  91. const isTabClosable = (tab: WorkTab): boolean => {
  92. return !tab.fixedTab
  93. }
  94. /**
  95. * 过滤不应展示在 worktab 中的父级占位路由。
  96. * `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/tenant`)。
  97. */
  98. const shouldIgnoreTab = (tab: Partial<WorkTab>): boolean => {
  99. if (tab.path === '/system') {
  100. return true
  101. }
  102. if (isWorkbenchRoute(tab.path)) {
  103. return true
  104. }
  105. const title = String(tab.title ?? tab.customTitle ?? '')
  106. return title === '工作台' || title === 'menus.dashboard.console'
  107. }
  108. /**
  109. * 标签全部关闭后的默认落点:当前一级模块首个二级菜单(无则回退首页路径)
  110. */
  111. const isPortalMultiTabMode = (): boolean => {
  112. const setting = useSettingStore()
  113. return setting.menuType === MenuTypeEnum.PORTAL && setting.showWorkTab
  114. }
  115. const resolveEmptyTabsPath = (): string => {
  116. const { homePath } = useCommon()
  117. const home = homePath.value || HOME_PAGE_PATH || '/home'
  118. if (isPortalMultiTabMode()) {
  119. return home
  120. }
  121. const menuStore = useMenuStore()
  122. const currentPath = String(
  123. router.currentRoute.value.meta.activePath || router.currentRoute.value.path
  124. )
  125. const fromModule = getPortalDefaultPathForRoute(menuStore.menuList, currentPath)
  126. if (fromModule) {
  127. return fromModule
  128. }
  129. const fromMenu = getPortalDefaultHomePath(menuStore.menuList)
  130. if (fromMenu) {
  131. return fromMenu
  132. }
  133. if (home && !isWorkbenchRoute(home)) {
  134. return home
  135. }
  136. return '/'
  137. }
  138. const overflowCheckHandler = ref<null | (() => boolean)>(null)
  139. const registerOverflowChecker = (fn: (() => boolean) | null) => {
  140. overflowCheckHandler.value = fn
  141. }
  142. const evictUntilNoOverflow = (): void => {
  143. if (!isPortalMultiTabMode()) return
  144. const checker = overflowCheckHandler.value
  145. if (!checker) return
  146. let guard = 0
  147. while (checker() && guard++ < 50) {
  148. if (!evictOldestClosableTab()) break
  149. }
  150. if (current.value.path && findTabIndex(String(current.value.path)) === -1) {
  151. const fallback = opened.value[opened.value.length - 1]
  152. if (fallback?.path) {
  153. current.value = fallback
  154. safeRouterPush(fallback)
  155. }
  156. }
  157. }
  158. /**
  159. * 安全的路由跳转
  160. */
  161. const safeRouterPush = (tab: Partial<WorkTab>): void => {
  162. if (!tab.path) {
  163. console.warn('尝试跳转到无效路径的标签页')
  164. return
  165. }
  166. try {
  167. router.push({
  168. path: tab.path,
  169. query: tab.query as LocationQueryRaw
  170. })
  171. } catch (error) {
  172. console.error('路由跳转失败:', error)
  173. }
  174. }
  175. /** 严格单页:showWorkTab 为 false 时仅保留当前页面并清理缓存 */
  176. const isSinglePageMode = (): boolean => !useSettingStore().showWorkTab
  177. /**
  178. * 将已打开标签压至单页(保留当前激活页,其余移出 KeepAlive)
  179. */
  180. const enforceSinglePageTabs = (): void => {
  181. if (!isSinglePageMode() || opened.value.length <= 1) {
  182. return
  183. }
  184. const activePath =
  185. current.value.path || router.currentRoute.value.meta.activePath || router.currentRoute.value.path
  186. const activeTab =
  187. opened.value.find((t) => t.path === activePath) ?? opened.value[opened.value.length - 1]
  188. const toRemove = opened.value.filter((t) => t.path !== activeTab?.path)
  189. markTabsToRemove(toRemove)
  190. opened.value = activeTab ? [activeTab] : []
  191. if (activeTab) {
  192. current.value = activeTab
  193. }
  194. }
  195. /**
  196. * 打开或激活一个选项卡
  197. * @param options.activate 默认 true;登录预开收藏时传 false,避免切走当前页
  198. */
  199. const openTab = (tab: WorkTab, options: OpenTabOptions = {}): void => {
  200. if (!tab.path) {
  201. console.warn('尝试打开无效的标签页')
  202. return
  203. }
  204. if (shouldIgnoreTab(tab)) return
  205. const activate = options.activate !== false
  206. const singlePage = isSinglePageMode()
  207. const portalMulti = isPortalMultiTabMode()
  208. // 从 keepAlive 排除列表中移除
  209. if (tab.name) {
  210. removeKeepAliveExclude(tab.name)
  211. }
  212. // 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找
  213. let existingIndex = -1
  214. if (tab.name) {
  215. existingIndex = opened.value.findIndex((t) => t.name === tab.name)
  216. }
  217. if (existingIndex === -1) {
  218. existingIndex = findTabIndex(tab.path)
  219. }
  220. if (existingIndex === -1) {
  221. const newTab = { ...tab, fixedTab: singlePage ? false : tab.fixedTab }
  222. if (singlePage) {
  223. markTabsToRemove(opened.value.filter((t) => t.path !== newTab.path))
  224. opened.value = [newTab]
  225. current.value = newTab
  226. return
  227. }
  228. // 非门户:保留固定数量上限;门户改由 DOM 溢出淘汰
  229. if (!portalMulti && !ensureCapacityForNewTab()) {
  230. return
  231. }
  232. const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length
  233. if (tab.fixedTab) {
  234. opened.value.splice(insertIndex, 0, newTab)
  235. } else {
  236. opened.value.push(newTab)
  237. }
  238. if (activate || !current.value.path) {
  239. current.value = newTab
  240. }
  241. } else {
  242. // 更新现有标签页(当动态路由参数或查询变更时,复用同一标签)
  243. const existingTab = opened.value[existingIndex]
  244. opened.value[existingIndex] = {
  245. ...existingTab,
  246. path: tab.path,
  247. params: tab.params,
  248. query: tab.query,
  249. title: tab.title || existingTab.title,
  250. fixedTab: singlePage ? false : (tab.fixedTab ?? existingTab.fixedTab),
  251. keepAlive: tab.keepAlive ?? existingTab.keepAlive,
  252. name: tab.name || existingTab.name,
  253. icon: tab.icon || existingTab.icon
  254. }
  255. if (activate) {
  256. current.value = opened.value[existingIndex]
  257. }
  258. if (singlePage) {
  259. enforceSinglePageTabs()
  260. }
  261. }
  262. }
  263. /**
  264. * 查找固定标签页的插入位置
  265. */
  266. const findFixedTabInsertIndex = (): number => {
  267. let insertIndex = 0
  268. for (let i = 0; i < opened.value.length; i++) {
  269. if (opened.value[i].fixedTab) {
  270. insertIndex = i + 1
  271. } else {
  272. break
  273. }
  274. }
  275. return insertIndex
  276. }
  277. /**
  278. * 关闭指定的选项卡
  279. */
  280. const removeTab = (path: string): void => {
  281. const targetTab = getTab(path)
  282. const targetIndex = findTabIndex(path)
  283. if (targetIndex === -1) {
  284. console.warn(`尝试关闭不存在的标签页: ${path}`)
  285. return
  286. }
  287. if (targetTab && !isTabClosable(targetTab)) {
  288. console.warn(`尝试关闭固定标签页: ${path}`)
  289. return
  290. }
  291. // 从标签页列表中移除
  292. opened.value.splice(targetIndex, 1)
  293. // 处理缓存排除
  294. if (targetTab?.name) {
  295. addKeepAliveExclude(targetTab)
  296. }
  297. // 如果关闭后无标签页,跳转侧栏第一个二级菜单
  298. if (!hasOpenedTabs.value) {
  299. current.value = {}
  300. safeRouterPush({ path: resolveEmptyTabsPath() })
  301. return
  302. }
  303. // 如果关闭的是当前激活标签,需要激活其他标签
  304. if (current.value.path === path) {
  305. const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex
  306. current.value = opened.value[newIndex]
  307. safeRouterPush(current.value)
  308. }
  309. }
  310. /**
  311. * 关闭左侧选项卡
  312. */
  313. const removeLeft = (path: string): void => {
  314. const targetIndex = findTabIndex(path)
  315. if (targetIndex === -1) {
  316. console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`)
  317. return
  318. }
  319. // 获取左侧可关闭的标签页
  320. const leftTabs = opened.value.slice(0, targetIndex)
  321. const closableLeftTabs = leftTabs.filter(isTabClosable)
  322. if (closableLeftTabs.length === 0) {
  323. console.warn('左侧没有可关闭的标签页')
  324. return
  325. }
  326. // 标记为缓存排除
  327. markTabsToRemove(closableLeftTabs)
  328. // 移除左侧可关闭的标签页
  329. opened.value = opened.value.filter(
  330. (tab, index) => index >= targetIndex || !isTabClosable(tab)
  331. )
  332. // 确保当前标签是激活状态
  333. const targetTab = getTab(path)
  334. if (targetTab) {
  335. current.value = targetTab
  336. }
  337. }
  338. /**
  339. * 关闭右侧选项卡
  340. */
  341. const removeRight = (path: string): void => {
  342. const targetIndex = findTabIndex(path)
  343. if (targetIndex === -1) {
  344. console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`)
  345. return
  346. }
  347. // 获取右侧可关闭的标签页
  348. const rightTabs = opened.value.slice(targetIndex + 1)
  349. const closableRightTabs = rightTabs.filter(isTabClosable)
  350. if (closableRightTabs.length === 0) {
  351. console.warn('右侧没有可关闭的标签页')
  352. return
  353. }
  354. // 标记为缓存排除
  355. markTabsToRemove(closableRightTabs)
  356. // 移除右侧可关闭的标签页
  357. opened.value = opened.value.filter(
  358. (tab, index) => index <= targetIndex || !isTabClosable(tab)
  359. )
  360. // 确保当前标签是激活状态
  361. const targetTab = getTab(path)
  362. if (targetTab) {
  363. current.value = targetTab
  364. }
  365. }
  366. /**
  367. * 关闭其他选项卡
  368. */
  369. const removeOthers = (path: string): void => {
  370. const targetTab = getTab(path)
  371. if (!targetTab) {
  372. console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`)
  373. return
  374. }
  375. // 获取其他可关闭的标签页
  376. const otherTabs = opened.value.filter((tab) => tab.path !== path)
  377. const closableTabs = otherTabs.filter(isTabClosable)
  378. if (closableTabs.length === 0) {
  379. console.warn('没有其他可关闭的标签页')
  380. return
  381. }
  382. // 标记为缓存排除
  383. markTabsToRemove(closableTabs)
  384. // 只保留当前标签和固定标签
  385. opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab))
  386. // 确保当前标签是激活状态
  387. current.value = targetTab
  388. }
  389. /**
  390. * 关闭所有可关闭的标签页
  391. */
  392. const removeAll = (): void => {
  393. const closableTabs = opened.value.filter(isTabClosable)
  394. if (closableTabs.length === 0) {
  395. console.warn('没有可关闭的标签页')
  396. return
  397. }
  398. markTabsToRemove(closableTabs)
  399. opened.value = opened.value.filter((tab) => !isTabClosable(tab))
  400. const fallbackPath = resolveEmptyTabsPath()
  401. if (!hasOpenedTabs.value) {
  402. current.value = {}
  403. safeRouterPush({ path: fallbackPath })
  404. return
  405. }
  406. const targetTab = opened.value[0]
  407. current.value = targetTab
  408. safeRouterPush(targetTab)
  409. }
  410. /**
  411. * 将指定选项卡添加到 keepAlive 排除列表中
  412. */
  413. const addKeepAliveExclude = (tab: WorkTab): void => {
  414. if (!tab.keepAlive || !tab.name) return
  415. if (!keepAliveExclude.value.includes(tab.name)) {
  416. keepAliveExclude.value.push(tab.name)
  417. }
  418. }
  419. /**
  420. * 从 keepAlive 排除列表中移除指定组件名称
  421. */
  422. const removeKeepAliveExclude = (name: string): void => {
  423. if (!name) return
  424. keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name)
  425. }
  426. /**
  427. * 关闭最早打开的可关闭标签(数组中最靠前的非 fixedTab)
  428. */
  429. const evictOldestClosableTab = (): boolean => {
  430. const index = opened.value.findIndex(isTabClosable)
  431. if (index === -1) {
  432. return false
  433. }
  434. const [removed] = opened.value.splice(index, 1)
  435. if (removed?.name) {
  436. addKeepAliveExclude(removed)
  437. }
  438. return true
  439. }
  440. /**
  441. * 新开标签前:保证 opened.length < MAX_OPEN_TABS
  442. */
  443. const ensureCapacityForNewTab = (): boolean => {
  444. while (opened.value.length >= MAX_OPEN_TABS) {
  445. if (!evictOldestClosableTab()) {
  446. console.warn(`工作标签已达上限 ${MAX_OPEN_TABS} 个,且均为固定标签,无法打开新页面`)
  447. return false
  448. }
  449. }
  450. return true
  451. }
  452. /**
  453. * 持久化恢复或校验后:将数量压回上限以内
  454. */
  455. const trimTabsToMaxOpen = (): void => {
  456. while (opened.value.length > MAX_OPEN_TABS) {
  457. if (!evictOldestClosableTab()) {
  458. break
  459. }
  460. }
  461. }
  462. /**
  463. * 将传入的一组选项卡的组件名称标记为排除缓存
  464. */
  465. const markTabsToRemove = (tabs: WorkTab[]): void => {
  466. tabs.forEach((tab) => {
  467. if (tab.name) {
  468. addKeepAliveExclude(tab)
  469. }
  470. })
  471. }
  472. /**
  473. * 切换指定标签页的固定状态
  474. */
  475. const toggleFixedTab = (path: string): void => {
  476. const targetIndex = findTabIndex(path)
  477. if (targetIndex === -1) {
  478. console.warn(`尝试切换不存在标签页的固定状态: ${path}`)
  479. return
  480. }
  481. const tab = { ...opened.value[targetIndex] }
  482. tab.fixedTab = !tab.fixedTab
  483. // 移除原位置
  484. opened.value.splice(targetIndex, 1)
  485. if (tab.fixedTab) {
  486. // 固定标签插入到所有固定标签的末尾
  487. const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab)
  488. const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex
  489. opened.value.splice(insertIndex, 0, tab)
  490. } else {
  491. // 非固定标签插入到所有固定标签后
  492. const fixedCount = opened.value.filter((t) => t.fixedTab).length
  493. opened.value.splice(fixedCount, 0, tab)
  494. }
  495. // 更新当前标签引用
  496. if (current.value.path === path) {
  497. current.value = tab
  498. }
  499. }
  500. /**
  501. * 验证工作台标签页的路由有效性
  502. */
  503. const validateWorktabs = (routerInstance: Router): void => {
  504. try {
  505. // 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径
  506. const isTabRouteValid = (tab: Partial<WorkTab>): boolean => {
  507. try {
  508. if (tab.name) {
  509. const routes = routerInstance.getRoutes()
  510. if (routes.some((r) => r.name === tab.name)) return true
  511. }
  512. if (tab.path) {
  513. const resolved = routerInstance.resolve({
  514. path: tab.path,
  515. query: (tab.query as LocationQueryRaw) || undefined
  516. })
  517. return resolved.matched.length > 0
  518. }
  519. return false
  520. } catch {
  521. return false
  522. }
  523. }
  524. // 过滤出有效的标签页
  525. const validTabs = opened.value.filter(
  526. (tab) => !shouldIgnoreTab(tab) && isTabRouteValid(tab)
  527. )
  528. if (validTabs.length !== opened.value.length) {
  529. console.warn('发现无效的标签页路由,已自动清理')
  530. opened.value = validTabs
  531. }
  532. if (isSinglePageMode()) {
  533. enforceSinglePageTabs()
  534. } else {
  535. trimTabsToMaxOpen()
  536. }
  537. // 验证当前激活标签的有效性
  538. const isCurrentValid =
  539. current.value && !shouldIgnoreTab(current.value) && isTabRouteValid(current.value)
  540. if (!isCurrentValid && validTabs.length > 0) {
  541. console.warn('当前激活标签无效,已自动切换')
  542. current.value = validTabs[0]
  543. } else if (!isCurrentValid) {
  544. current.value = {}
  545. }
  546. const activePath = routerInstance.currentRoute.value.path
  547. if (isWorkbenchRoute(activePath) || shouldIgnoreTab(current.value)) {
  548. const fallback = resolveEmptyTabsPath()
  549. if (fallback && fallback !== activePath) {
  550. safeRouterPush({ path: fallback })
  551. }
  552. }
  553. } catch (error) {
  554. console.error('验证工作台标签页失败:', error)
  555. }
  556. }
  557. /**
  558. * 清空所有状态(用于登出等场景)
  559. */
  560. const clearAll = (): void => {
  561. current.value = {}
  562. opened.value = []
  563. keepAliveExclude.value = []
  564. }
  565. /**
  566. * 获取状态快照(用于持久化存储)
  567. */
  568. const getStateSnapshot = (): WorktabState => {
  569. return {
  570. current: { ...current.value },
  571. opened: [...opened.value],
  572. keepAliveExclude: [...keepAliveExclude.value]
  573. }
  574. }
  575. /**
  576. * 获取标签页标题
  577. */
  578. const getTabTitle = (path: string): WorkTab | undefined => {
  579. const tab = getTab(path)
  580. return tab
  581. }
  582. /**
  583. * 更新标签页标题
  584. */
  585. const updateTabTitle = (path: string, title: string): void => {
  586. const tab = getTab(path)
  587. if (tab) {
  588. tab.customTitle = title
  589. }
  590. }
  591. /**
  592. * 重置标签页标题
  593. */
  594. const resetTabTitle = (path: string): void => {
  595. const tab = getTab(path)
  596. if (tab) {
  597. tab.customTitle = ''
  598. }
  599. }
  600. return {
  601. // 状态
  602. current,
  603. opened,
  604. keepAliveExclude,
  605. // 计算属性
  606. hasOpenedTabs,
  607. hasMultipleTabs,
  608. currentTabIndex,
  609. // 方法
  610. openTab,
  611. removeTab,
  612. removeLeft,
  613. removeRight,
  614. removeOthers,
  615. removeAll,
  616. toggleFixedTab,
  617. validateWorktabs,
  618. enforceSinglePageTabs,
  619. registerOverflowChecker,
  620. evictUntilNoOverflow,
  621. clearAll,
  622. getStateSnapshot,
  623. // 工具方法
  624. findTabIndex,
  625. getTab,
  626. isTabClosable,
  627. addKeepAliveExclude,
  628. removeKeepAliveExclude,
  629. markTabsToRemove,
  630. getTabTitle,
  631. updateTabTitle,
  632. resetTabTitle
  633. }
  634. },
  635. {
  636. persist: {
  637. key: 'worktab',
  638. storage: localStorage
  639. }
  640. }
  641. )