index.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <script setup lang="ts">
  2. import { isEmpty } from 'lodash-es'
  3. import { computed, nextTick, onMounted, ref, watch, inject } from 'vue'
  4. import { RouterView } from 'vue-router'
  5. import { mediaQueryInjectionKey } from '@/injection'
  6. import router from '@/router'
  7. import { usePreferencesStore } from '@/stores'
  8. import { useTabsStore } from '@/stores'
  9. import type { Tab } from '@/stores'
  10. import type { RouteLocationNormalizedLoaded } from 'vue-router'
  11. defineOptions({
  12. name: 'MainLayout',
  13. })
  14. const mediaQueryInject = inject(mediaQueryInjectionKey, null)
  15. const layoutsRouteRedirect = router.getRoutes().find((item) => item.name === 'layouts')?.redirect
  16. const tabsStore = useTabsStore()
  17. const preferencesStore = usePreferencesStore()
  18. const { createTab, setTabActivePath } = tabsStore
  19. const navigationTransitionName = ref('scale')
  20. const isMounted = ref(false)
  21. const keepAliveTabs = computed(() => {
  22. return tabsStore.tabs.filter((tab) => tab.keepAlive).map((tab) => tab.componentName ?? '')
  23. })
  24. let oldTabs: Tab[] = []
  25. function createTabFromRoute(route: RouteLocationNormalizedLoaded) {
  26. const {
  27. icon = 'iconify ph--browser',
  28. title: label = '未命名标签',
  29. renderTabTitle,
  30. componentName,
  31. pinned,
  32. } = route.meta
  33. const { fullPath, name, params } = route
  34. const title = renderTabTitle ? renderTabTitle(params) : label
  35. createTab({
  36. path: fullPath,
  37. icon,
  38. title,
  39. name,
  40. componentName,
  41. pinned,
  42. })
  43. }
  44. watch(
  45. (): [Tab[], string] => [tabsStore.tabs, tabsStore.tabActivePath],
  46. ([newTabs, newTabActivePath], [, oldTabActivePath]) => {
  47. if (
  48. newTabActivePath &&
  49. newTabActivePath !== oldTabActivePath &&
  50. newTabActivePath !== router.currentRoute.value.fullPath
  51. ) {
  52. if (
  53. layoutsRouteRedirect &&
  54. layoutsRouteRedirect === router.currentRoute.value.path &&
  55. newTabActivePath === '/'
  56. ) {
  57. router.go(0)
  58. } else {
  59. router.push(newTabActivePath)
  60. }
  61. }
  62. if (!preferencesStore.preferences.enableNavigationTransition) return
  63. if (!preferencesStore.preferences.showTabs || mediaQueryInject?.sm.value) {
  64. navigationTransitionName.value = 'scale'
  65. return
  66. }
  67. const oldActiveIndex = oldTabs.findIndex((item) => item.path === oldTabActivePath)
  68. const newActiveIndex = newTabs.findIndex((item) => item.path === newTabActivePath)
  69. if (oldTabs.length < newTabs.length || oldActiveIndex === -1 || newActiveIndex === -1) {
  70. navigationTransitionName.value = 'scale'
  71. } else if (oldTabs.length > newTabs.length) {
  72. navigationTransitionName.value =
  73. oldActiveIndex > newActiveIndex ? 'scale-slider-left' : 'scale-slider-right'
  74. } else if (oldTabActivePath !== newTabActivePath) {
  75. navigationTransitionName.value =
  76. oldActiveIndex > newActiveIndex ? 'slider-left' : 'slider-right'
  77. }
  78. oldTabs = [...newTabs]
  79. },
  80. )
  81. watch(
  82. () => preferencesStore.preferences.shouldRefreshTab,
  83. (shouldRefresh) => {
  84. if (shouldRefresh) {
  85. navigationTransitionName.value = 'shake'
  86. nextTick(() => {
  87. preferencesStore.modify({
  88. shouldRefreshTab: false,
  89. })
  90. })
  91. }
  92. },
  93. )
  94. watch(
  95. () => router.currentRoute.value,
  96. (newRoute, oldRoute) => {
  97. if (newRoute.fullPath !== oldRoute?.fullPath) {
  98. const { showTab, enableMultiTab } = newRoute.meta
  99. const targetPath = enableMultiTab ? newRoute.fullPath : newRoute.path
  100. const findTab = tabsStore.tabs.find((item) => item.path === targetPath)
  101. if (!isEmpty(findTab)) {
  102. setTabActivePath(findTab.path)
  103. } else if (showTab) {
  104. createTabFromRoute(newRoute)
  105. } else {
  106. setTabActivePath('')
  107. }
  108. }
  109. },
  110. {
  111. immediate: true,
  112. },
  113. )
  114. onMounted(() => {
  115. oldTabs = [...tabsStore.tabs]
  116. isMounted.value = true
  117. })
  118. </script>
  119. <template>
  120. <RouterView
  121. v-if="preferencesStore.preferences.enableNavigationTransition"
  122. v-slot="{ Component, route }"
  123. >
  124. <Transition
  125. type="transition"
  126. :name="navigationTransitionName"
  127. >
  128. <KeepAlive :include="keepAliveTabs">
  129. <component
  130. :is="Component"
  131. v-if="isMounted && !preferencesStore.preferences.shouldRefreshTab"
  132. :key="route.path + JSON.stringify(route.query)"
  133. />
  134. </KeepAlive>
  135. </Transition>
  136. </RouterView>
  137. <RouterView v-else />
  138. </template>
  139. <style scoped>
  140. .slider-left-enter-active,
  141. .slider-right-enter-active,
  142. .slider-left-leave-active,
  143. .slider-right-leave-active {
  144. position: absolute;
  145. width: 100%;
  146. transition: translate 500ms var(--cubic-bezier-ease-in-out) 75ms;
  147. will-change: translate;
  148. }
  149. .slider-left-enter-from,
  150. .slider-right-leave-to {
  151. translate: -100%;
  152. }
  153. .slider-left-leave-to,
  154. .slider-right-enter-from {
  155. translate: 100%;
  156. }
  157. .slider-left-enter-to,
  158. .slider-left-leave-from,
  159. .slider-right-enter-to,
  160. .slider-right-leave-from {
  161. translate: 0;
  162. }
  163. .scale-slider-left-enter-active,
  164. .scale-slider-left-leave-active,
  165. .scale-slider-right-enter-active,
  166. .scale-slider-right-leave-active {
  167. position: absolute;
  168. width: 100%;
  169. transition:
  170. scale 500ms var(--cubic-bezier-ease-in-out) 75ms,
  171. translate 500ms var(--cubic-bezier-ease-in-out) 75ms,
  172. opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
  173. will-change: scale, translate, opacity;
  174. }
  175. .scale-slider-left-leave-to,
  176. .scale-slider-right-leave-to {
  177. scale: 0.5;
  178. opacity: 0;
  179. }
  180. .scale-slider-left-enter-from {
  181. translate: -100%;
  182. }
  183. .scale-slider-right-enter-from {
  184. translate: 100%;
  185. }
  186. .scale-enter-active,
  187. .scale-leave-active {
  188. position: absolute;
  189. width: 100%;
  190. transition:
  191. scale 300ms var(--cubic-bezier-ease-in-out) 75ms,
  192. opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
  193. will-change: scale, opacity;
  194. }
  195. .scale-enter-from,
  196. .scale-leave-to {
  197. scale: 0.5;
  198. opacity: 0;
  199. }
  200. .shake-enter-active {
  201. animation: shake var(--cubic-bezier-ease-in-out) 500ms;
  202. }
  203. @keyframes shake {
  204. 0% {
  205. translate: 0 0;
  206. rotate: 0deg;
  207. }
  208. 8% {
  209. translate: -2px -1px;
  210. rotate: -0.6deg;
  211. }
  212. 16% {
  213. translate: 1px -2px;
  214. rotate: 0.9deg;
  215. }
  216. 24% {
  217. translate: -1px 2px;
  218. rotate: -0.5deg;
  219. }
  220. 32% {
  221. translate: 2px -1px;
  222. rotate: 0.7deg;
  223. }
  224. 40% {
  225. translate: -1px -2px;
  226. rotate: -0.8deg;
  227. }
  228. 48% {
  229. translate: 1px 2px;
  230. rotate: 0.4deg;
  231. }
  232. 56% {
  233. translate: -2px 1px;
  234. rotate: -0.7deg;
  235. }
  236. 64% {
  237. translate: 2px -1px;
  238. rotate: 0.8deg;
  239. }
  240. 72% {
  241. translate: -1px -1px;
  242. rotate: -0.5deg;
  243. }
  244. 80% {
  245. translate: 2px 1px;
  246. rotate: 0.6deg;
  247. }
  248. 88% {
  249. translate: -1px 1px;
  250. rotate: -0.4deg;
  251. }
  252. 96% {
  253. translate: 1px -1px;
  254. rotate: 0.3deg;
  255. }
  256. 100% {
  257. translate: 0 0;
  258. rotate: 0deg;
  259. }
  260. }
  261. </style>